mirror of
https://github.com/h44z/wg-portal.git
synced 2025-08-09 15:02:24 +00:00
Start Github V2 branch
This commit is contained in:
parent
3c2c7f325b
commit
94f0b26304
@ -52,29 +52,6 @@ jobs:
|
||||
working_directory: ~/repo
|
||||
docker:
|
||||
- image: cimg/go:1.19
|
||||
build-116: # just to validate compatibility with minimum go version
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-116-v4-{{ checksum "go.sum" }}
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: |
|
||||
make build-dependencies
|
||||
- save_cache:
|
||||
key: go-mod-116-v4-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- "~/go/pkg/mod"
|
||||
- run:
|
||||
name: Build
|
||||
command: |
|
||||
VERSION=$CIRCLE_BRANCH
|
||||
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
|
||||
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build
|
||||
working_directory: ~/repo116
|
||||
docker:
|
||||
- image: cimg/go:1.16
|
||||
|
||||
workflows:
|
||||
build-and-release:
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -28,10 +28,11 @@
|
||||
out/
|
||||
dist/
|
||||
data/
|
||||
docker_images/
|
||||
ssh.key
|
||||
.testCoverage.txt
|
||||
wg_portal.db
|
||||
sqlite.db
|
||||
go.sum
|
||||
swagger.json
|
||||
swagger.yaml
|
||||
/config.yml
|
11
.run/swag_build_tool.run.xml
Normal file
11
.run/swag_build_tool.run.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="swag_build_tool" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="wg-portal" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/h44z/wg-portal/internal/ports/api/build_tool" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/internal/ports/api/build_tool/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
17
.run/wg-portal-migrate.run.xml
Normal file
17
.run/wg-portal-migrate.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="wg-portal-migrate" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="wg-portal" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="-migrateFrom=test_source.db" />
|
||||
<envs>
|
||||
<env name="SESSION_SECRET" value="extremlybad" />
|
||||
<env name="LOG_LEVEL" value="trace" />
|
||||
</envs>
|
||||
<sudo value="true" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/h44z/wg-portal/cmd/wg-portal" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/cmd/wg-portal/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
16
.run/wg-portal.run.xml
Normal file
16
.run/wg-portal.run.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="wg-portal" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="wg-portal" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<envs>
|
||||
<env name="SESSION_SECRET" value="extremlybad" />
|
||||
<env name="LOG_LEVEL" value="trace" />
|
||||
</envs>
|
||||
<sudo value="true" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/h44z/wg-portal/cmd/wg-portal" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/cmd/wg-portal/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
6
Makefile
6
Makefile
@ -5,6 +5,7 @@ GOFILES:=$(shell go list ./... | grep -v /vendor/)
|
||||
BUILDDIR=dist
|
||||
BINARIES=$(subst cmd/,,$(wildcard cmd/*))
|
||||
IMAGE=h44z/wg-portal
|
||||
NPMCMD=npm
|
||||
|
||||
all: help
|
||||
|
||||
@ -122,3 +123,8 @@ build-dependencies:
|
||||
@mkdir -p $(BUILDDIR)
|
||||
cp scripts/wg-portal.service $(BUILDDIR)
|
||||
cp scripts/wg-portal.env $(BUILDDIR)
|
||||
cd internal/ports/api/core/frontend; $(NPMCMD) install
|
||||
|
||||
#< frontend: Build Vue.js frontend
|
||||
frontend:
|
||||
cd internal/ports/api/core/frontend; $(NPMCMD) run build
|
@ -1,190 +0,0 @@
|
||||
// Lux 4.5.3
|
||||
// Bootswatch
|
||||
|
||||
|
||||
// Variables ===================================================================
|
||||
|
||||
$web-font-path: "https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600&display=swap" !default;
|
||||
@import url($web-font-path);
|
||||
|
||||
// Navbar ======================================================================
|
||||
|
||||
.navbar {
|
||||
font-size: $font-size-sm;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
|
||||
&-nav {
|
||||
.nav-link {
|
||||
padding-top: .715rem;
|
||||
padding-bottom: .715rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-brand {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: theme-color("primary") !important;
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
|
||||
&.navbar-fixed-top {
|
||||
border-width: 0 0 1px;
|
||||
}
|
||||
|
||||
&.navbar-bottom-top {
|
||||
border-width: 1px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
// Buttons =====================================================================
|
||||
|
||||
.btn {
|
||||
font-size: $font-size-sm;
|
||||
text-transform: uppercase;
|
||||
|
||||
&-sm {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&-warning {
|
||||
&,
|
||||
&:hover,
|
||||
&:not([disabled]):not(.disabled):active,
|
||||
&:focus {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
border-color: $gray-600;
|
||||
color: $gray-600;
|
||||
|
||||
&:not([disabled]):not(.disabled):hover,
|
||||
&:not([disabled]):not(.disabled):focus,
|
||||
&:not([disabled]):not(.disabled):active {
|
||||
background-color: $gray-400;
|
||||
border-color: $gray-400;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:not([disabled]):not(.disabled):focus {
|
||||
box-shadow: 0 0 0 .2rem rgba($gray-400, .5);
|
||||
}
|
||||
}
|
||||
|
||||
[class*="btn-outline-"] {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.border-secondary {
|
||||
border: 1px solid $gray-400 !important;
|
||||
}
|
||||
|
||||
// Typography ==================================================================
|
||||
|
||||
body {
|
||||
font-weight: 200;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: $body-color !important;
|
||||
}
|
||||
|
||||
// Tables ======================================================================
|
||||
|
||||
th {
|
||||
font-size: $font-size-sm;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table {
|
||||
th,
|
||||
td {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
&-sm {
|
||||
th,
|
||||
td {
|
||||
padding: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forms =======================================================================
|
||||
|
||||
.custom-switch {
|
||||
.custom-control-label {
|
||||
&::after {
|
||||
top: add(.15625rem, 2px);
|
||||
left: add(-2.25rem, 2px);
|
||||
width: subtract(1rem, 4px);
|
||||
height: subtract(1rem, 4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navs ========================================================================
|
||||
|
||||
.dropdown-menu {
|
||||
font-size: $font-size-sm;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
// Indicators ==================================================================
|
||||
|
||||
.badge {
|
||||
padding-top: .28rem;
|
||||
|
||||
&-pill {
|
||||
border-radius: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Containers ==================================================================
|
||||
|
||||
.list-group-item {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.h1,
|
||||
.h2,
|
||||
.h3,
|
||||
.h4,
|
||||
.h5,
|
||||
.h6 {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
&-title,
|
||||
&-header {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
// Lux 4.5.3
|
||||
// Bootswatch
|
||||
|
||||
//
|
||||
// Color system
|
||||
//
|
||||
|
||||
$white: #fff !default;
|
||||
$gray-100: #f8f9fa !default;
|
||||
$gray-200: #f7f7f9 !default;
|
||||
$gray-300: #eceeef !default;
|
||||
$gray-400: #ced4da !default;
|
||||
$gray-500: #adb5bd !default;
|
||||
$gray-600: #919aa1 !default;
|
||||
$gray-700: #55595c !default;
|
||||
$gray-800: #343a40 !default;
|
||||
$gray-900: #1a1a1a !default;
|
||||
$black: #000 !default;
|
||||
|
||||
$blue: #007bff !default;
|
||||
$indigo: #6610f2 !default;
|
||||
$purple: #6f42c1 !default;
|
||||
$pink: #e83e8c !default;
|
||||
$red: #d9534f !default;
|
||||
$orange: #fd7e14 !default;
|
||||
$yellow: #f0ad4e !default;
|
||||
$green: #4bbf73 !default;
|
||||
$teal: #20c997 !default;
|
||||
$cyan: #1f9bcf !default;
|
||||
|
||||
$primary: $gray-900 !default;
|
||||
$secondary: $white !default;
|
||||
$success: $green !default;
|
||||
$info: $cyan !default;
|
||||
$warning: $yellow !default;
|
||||
$danger: $red !default;
|
||||
$light: $white !default;
|
||||
$dark: $gray-800 !default;
|
||||
|
||||
$yiq-contrasted-threshold: 185 !default;
|
||||
|
||||
// Options
|
||||
|
||||
$enable-rounded: false !default;
|
||||
|
||||
// Body
|
||||
|
||||
$body-color: $gray-700 !default;
|
||||
|
||||
// Fonts
|
||||
|
||||
// stylelint-disable-next-line value-keyword-case
|
||||
$font-family-sans-serif: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
|
||||
$font-size-base: .875rem !default;
|
||||
$h1-font-size: 2rem !default;
|
||||
$h2-font-size: 1.75rem !default;
|
||||
$h3-font-size: 1.5rem !default;
|
||||
$h4-font-size: 1.25rem !default;
|
||||
$h5-font-size: 1rem !default;
|
||||
$h6-font-size: .75rem !default;
|
||||
$headings-font-weight: 600 !default;
|
||||
$headings-color: $gray-900 !default;
|
||||
|
||||
// Tables
|
||||
|
||||
$table-border-color: rgba(0, 0, 0, .05) !default;
|
||||
|
||||
// Buttons + Forms
|
||||
|
||||
$input-btn-border-width: 0 !default;
|
||||
|
||||
// Buttons
|
||||
|
||||
$btn-line-height: 1.5rem !default;
|
||||
$input-btn-padding-y: .75rem !default;
|
||||
$input-btn-padding-x: 1.5rem !default;
|
||||
$input-btn-padding-y-sm: .5rem !default;
|
||||
$input-btn-padding-x-sm: 1rem !default;
|
||||
$input-btn-padding-y-lg: 2rem !default;
|
||||
$input-btn-padding-x-lg: 2rem !default;
|
||||
$btn-font-weight: 600 !default;
|
||||
|
||||
// Forms
|
||||
|
||||
$input-line-height: 1.5 !default;
|
||||
$input-bg: $gray-200 !default;
|
||||
$input-disabled-bg: $gray-300 !default;
|
||||
$input-group-addon-bg: $gray-300 !default;
|
||||
|
||||
// Navbar
|
||||
|
||||
$navbar-padding-y: 1.5rem !default;
|
||||
$navbar-dark-hover-color: $white !default;
|
||||
$navbar-light-color: rgba($black, .3) !default;
|
||||
$navbar-light-hover-color: $gray-900 !default;
|
||||
$navbar-light-active-color: $gray-900 !default;
|
||||
|
||||
// Pagination
|
||||
|
||||
$pagination-border-color: transparent !default;
|
||||
$pagination-hover-border-color: $pagination-border-color !default;
|
||||
$pagination-disabled-border-color: $pagination-border-color !default;
|
||||
|
||||
// Breadcrumbs
|
||||
|
||||
$breadcrumb-bg: transparent !default;
|
5
assets/css/bootstrap-tokenfield.min.css
vendored
5
assets/css/bootstrap-tokenfield.min.css
vendored
@ -1,5 +0,0 @@
|
||||
/*!
|
||||
* bootstrap-tokenfield
|
||||
* https://github.com/sliptree/bootstrap-tokenfield
|
||||
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
|
||||
*/@-webkit-keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}@-moz-keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}@keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}.tokenfield{height:auto;min-height:34px;padding-bottom:0}.tokenfield.focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.tokenfield .token{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block;border:1px solid #d9d9d9;background-color:#ededed;white-space:nowrap;margin:-1px 5px 5px 0;height:22px;vertical-align:top;cursor:default}.tokenfield .token:hover{border-color:#b9b9b9}.tokenfield .token.active{border-color:#52a8ec;border-color:rgba(82,168,236,.8)}.tokenfield .token.duplicate{border-color:#ebccd1;-webkit-animation-name:blink;animation-name:blink;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-direction:normal;animation-direction:normal;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.tokenfield .token.invalid{background:0 0;border:1px solid transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border-bottom:1px dotted #d9534f}.tokenfield .token.invalid.active{background:#ededed;border:1px solid #ededed;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.tokenfield .token .token-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;padding-left:4px;vertical-align:top}.tokenfield .token .close{font-family:Arial;display:inline-block;line-height:100%;font-size:1.1em;line-height:1.49em;margin-left:5px;float:none;height:100%;vertical-align:top;padding-right:4px}.tokenfield .token-input{background:0 0;width:60px;min-width:60px;border:0;height:20px;padding:0;margin-bottom:6px;-webkit-box-shadow:none;box-shadow:none}.tokenfield .token-input:focus{border-color:transparent;outline:0;-webkit-box-shadow:none;box-shadow:none}.tokenfield.disabled{cursor:not-allowed;background-color:#eee}.tokenfield.disabled .token-input{cursor:not-allowed}.tokenfield.disabled .token:hover{cursor:not-allowed;border-color:#d9d9d9}.tokenfield.disabled .token:hover .close{cursor:not-allowed;opacity:.2;filter:alpha(opacity=20)}.has-warning .tokenfield.focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-error .tokenfield.focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-success .tokenfield.focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.tokenfield.input-sm,.input-group-sm .tokenfield{min-height:30px;padding-bottom:0}.input-group-sm .token,.tokenfield.input-sm .token{height:20px;margin-bottom:4px}.input-group-sm .token-input,.tokenfield.input-sm .token-input{height:18px;margin-bottom:5px}.tokenfield.input-lg,.input-group-lg .tokenfield{height:auto;min-height:45px;padding-bottom:4px}.input-group-lg .token,.tokenfield.input-lg .token{height:25px}.input-group-lg .token-label,.tokenfield.input-lg .token-label{line-height:23px}.input-group-lg .token .close,.tokenfield.input-lg .token .close{line-height:1.3em}.input-group-lg .token-input,.tokenfield.input-lg .token-input{height:23px;line-height:23px;margin-bottom:6px;vertical-align:top}.tokenfield.rtl{direction:rtl;text-align:right}.tokenfield.rtl .token{margin:-1px 0 5px 5px}.tokenfield.rtl .token .token-label{padding-left:0;padding-right:4px}
|
@ -1,118 +0,0 @@
|
||||
/* THEME STYLE */
|
||||
pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768px){body>.navbar-transparent{box-shadow:none}body>.navbar-transparent .navbar-nav>.open>a{box-shadow:none}}#home,#help{font-size:.9rem}#home .navbar,#help .navbar{background:#349aed;background:linear-gradient(145deg, #349aed 50%, #34d8ed 100%);transition:box-shadow 200ms ease-in}#home .navbar-transparent,#help .navbar-transparent{background:none !important;box-shadow:none}#home .navbar-brand .nav-link,#help .navbar-brand .nav-link{display:inline-block;margin-right:-30px}#home .nav-link,#help .nav-link{text-transform:uppercase;font-weight:500;color:#fff}#home{padding-top:0}#home .btn{padding:.6rem .55rem .5rem;box-shadow:none;font-size:.7rem;font-weight:500}.bs-docs-section{margin-top:4em}.bs-docs-section .page-header h1{padding:2rem 0;font-size:3rem}.dropdown-menu.show[aria-labelledby="themes"]{display:-ms-flexbox;display:flex;width:420px;-ms-flex-wrap:wrap;flex-wrap:wrap}.dropdown-menu.show[aria-labelledby="themes"] .dropdown-item{width:33.333%}.dropdown-menu.show[aria-labelledby="themes"] .dropdown-item:first-child{width:100%}.bs-component{position:relative}.bs-component+.bs-component{margin-top:1rem}.bs-component .card{margin-bottom:1rem}.bs-component .modal{position:relative;top:auto;right:auto;left:auto;bottom:auto;z-index:1;display:block}.bs-component .modal-dialog{width:90%}.bs-component .popover{position:relative;display:inline-block;width:220px;margin:20px}.source-button{display:none;position:absolute;top:0;right:0;z-index:100;font-weight:700}.source-button:hover{cursor:pointer}.bs-component:hover .source-button{display:block}#source-modal pre{max-height:calc(100vh - 11rem);background-color:rgba(0,0,0,0.7);color:rgba(255,255,255,0.7)}.nav-tabs{margin-bottom:15px}.progress{margin-bottom:10px}#footer{margin:5em 0}#footer li{float:left;margin-right:1.5em;margin-bottom:1.5em}#footer p{clear:left;margin-bottom:0}.splash{padding:12em 0 6em;background:#349aed;background:linear-gradient(145deg, #349aed 50%, #34d8ed 100%);color:#fff;text-align:center}.splash .logo{width:160px}.splash h1{font-size:3em;color:#fff}.splash #social{margin:2em 0 3em}.splash .alert{margin:2em 0;border:none}.splash .sponsor a{color:#fff}.section-tout{padding:6em 0 1em;border-bottom:1px solid rgba(0,0,0,0.05);background-color:#eaf1f1;text-align:center}.section-tout .icon{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;width:80px;height:80px;margin:0 auto 1rem;background:#349aed;background:linear-gradient(145deg, #3b9cea 50%, #3db8eb 100%);border-radius:50%;font-size:2rem;color:rgba(0,0,0,0.5)}.section-tout p{margin-bottom:5em}.section-preview{padding:4em 0}.section-preview .preview{margin-bottom:4em;background-color:#eaf1f1}.section-preview .preview .image{position:relative}.section-preview .preview .image::before{box-shadow:inset 0 0 0 1px rgba(0,0,0,0.1);position:absolute;top:0;left:0;width:100%;height:100%;content:"";pointer-events:none}.section-preview .preview .options{padding:2em;border:1px solid rgba(0,0,0,0.05);border-top:none;text-align:center}.section-preview .preview .options p{margin-bottom:2em}.section-preview .dropdown-menu{text-align:left}.section-preview .lead{margin-bottom:2em}.sponsor #carbonads{max-width:240px;margin:0 auto}.sponsor .carbon-text{display:block;margin-top:1em;font-size:12px}.sponsor .carbon-poweredby{float:right;margin-top:1em;font-size:10px}@media (max-width: 767px){.splash{padding-top:8em}.splash .logo{width:100px}.splash h1{font-size:2em}#banner{margin-bottom:2em;text-align:center}}
|
||||
|
||||
/* CUSTOM STYLE */
|
||||
|
||||
/* Start collapsable table
|
||||
-------------------------------------------------- */
|
||||
|
||||
.hiddenRow, .hiddenCell {
|
||||
padding: 0px!important;
|
||||
border-top: 0px!important;
|
||||
}
|
||||
|
||||
.collapsedRow .col-md-6{
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
.collapsedRow {
|
||||
padding: 10px 0px;
|
||||
border-top: 1px solid lightgray;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.collapse-indicator {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.collapse-indicator:after {
|
||||
font-weight: 900;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
content: "\f056";
|
||||
}
|
||||
.collapse-indicator.collapsed:after {
|
||||
font-weight: 900;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
content: "\f055";
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
End collapsable table*/
|
||||
|
||||
.jumbotron-home {
|
||||
padding: 1rem 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.jumbotron-home {
|
||||
padding: 2rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.container, .container-lg, .container-md, .container-sm, .container-xl {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
.device-status-table {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand > img {
|
||||
height: 2rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.disabled-peer {
|
||||
color: #d03131;
|
||||
}
|
||||
|
||||
.expiring-peer {
|
||||
color: #d09d12;
|
||||
}
|
||||
|
||||
.tokenfield .token {
|
||||
border-radius: 0px;
|
||||
border: 1px solid #1a1a1a;
|
||||
color: #1a1a1a;
|
||||
background-color: #f7f7f9;
|
||||
margin: -4px 5px 5px 0;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.form-group.required label:after {
|
||||
content:"*";
|
||||
color:red;
|
||||
}
|
||||
|
||||
a.advanced-settings:before {
|
||||
content: "Hide";
|
||||
}
|
||||
|
||||
a.advanced-settings.collapsed:before {
|
||||
content: "Show";
|
||||
}
|
||||
|
||||
.form-group.global-config label:after, .custom-control.global-config label:after {
|
||||
content: "g";
|
||||
color: #0057bb;
|
||||
font-size: xx-small;
|
||||
top: -5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.text-blue {
|
||||
color: #0057bb;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.pull-right-lg {
|
||||
float: right;
|
||||
}
|
||||
}
|
7
assets/css/jquery-ui.min.css
vendored
7
assets/css/jquery-ui.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1,8 +0,0 @@
|
||||
.navbar {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand > img {
|
||||
height: 2rem;
|
||||
width: auto;
|
||||
}
|
5
assets/css/tokenfield-typeahead.min.css
vendored
5
assets/css/tokenfield-typeahead.min.css
vendored
@ -1,5 +0,0 @@
|
||||
/*!
|
||||
* bootstrap-tokenfield
|
||||
* https://github.com/sliptree/bootstrap-tokenfield
|
||||
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
|
||||
*/.twitter-typeahead{width:100%;position:relative;vertical-align:top}.twitter-typeahead .tt-input,.twitter-typeahead .tt-hint{margin:0;width:100%;vertical-align:middle;background-color:#fff}.twitter-typeahead .tt-hint{color:#999;z-index:1;border:1px solid transparent}.twitter-typeahead .tt-input{color:#555;z-index:2}.twitter-typeahead .tt-input,.twitter-typeahead .tt-hint{height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429}.twitter-typeahead .input-sm.tt-input,.twitter-typeahead .hint-sm.tt-hint{border-radius:3px}.twitter-typeahead .input-lg.tt-input,.twitter-typeahead .hint-lg.tt-hint{border-radius:6px}.input-group .twitter-typeahead:first-child .tt-input,.input-group .twitter-typeahead:first-child .tt-hint{border-radius:4px 0 0 4px!important}.input-group .twitter-typeahead:last-child .tt-input,.input-group .twitter-typeahead:last-child .tt-hint{border-radius:0 4px 4px 0!important}.input-group.input-group-sm .twitter-typeahead:first-child .tt-input,.input-group.input-group-sm .twitter-typeahead:first-child .tt-hint{border-radius:3px 0 0 3px!important}.input-group.input-group-sm .twitter-typeahead:last-child .tt-input,.input-group.input-group-sm .twitter-typeahead:last-child .tt-hint{border-radius:0 3px 3px 0!important}.input-sm.tt-input,.hint-sm.tt-hint,.input-group.input-group-sm .tt-input,.input-group.input-group-sm .tt-hint{height:30px;padding:5px 10px;font-size:12px;line-height:1.5}.input-group.input-group-lg .twitter-typeahead:first-child .tt-input,.input-group.input-group-lg .twitter-typeahead:first-child .tt-hint{border-radius:6px 0 0 6px!important}.input-group.input-group-lg .twitter-typeahead:last-child .tt-input,.input-group.input-group-lg .twitter-typeahead:last-child .tt-hint{border-radius:0 6px 6px 0!important}.input-lg.tt-input,.hint-lg.tt-hint,.input-group.input-group-lg .tt-input,.input-group.input-group-lg .tt-hint{height:45px;padding:10px 16px;font-size:18px;line-height:1.33}.tt-dropdown-menu{width:100%;min-width:160px;margin-top:2px;padding:5px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);*border-right-width:2px;*border-bottom-width:2px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.tt-suggestion{display:block;padding:3px 20px}.tt-suggestion.tt-cursor{color:#262626;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.tt-suggestion.tt-cursor a{color:#fff}.tt-suggestion p{margin:0}.tokenfield .twitter-typeahead{width:auto}.tokenfield .twitter-typeahead .tt-hint{padding:0;height:20px}.tokenfield.input-sm .twitter-typeahead .tt-input,.tokenfield.input-sm .twitter-typeahead .tt-hint{height:18px;font-size:12px;line-height:1.5}.tokenfield.input-lg .twitter-typeahead .tt-input,.tokenfield.input-lg .twitter-typeahead .tt-hint{height:23px;font-size:18px;line-height:1.33}.tokenfield .twitter-typeahead .tt-suggestions{font-size:14px}
|
4
assets/fonts/font-awesome.min.css
vendored
4
assets/fonts/font-awesome.min.css
vendored
File diff suppressed because one or more lines are too long
13
assets/js/bootstrap-confirmation.min.js
vendored
13
assets/js/bootstrap-confirmation.min.js
vendored
File diff suppressed because one or more lines are too long
7
assets/js/bootstrap-tokenfield.min.js
vendored
7
assets/js/bootstrap-tokenfield.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,39 +0,0 @@
|
||||
(function($) {
|
||||
"use strict"; // Start of use strict
|
||||
|
||||
// Smooth scrolling using jQuery easing
|
||||
$(document).on('click', 'a.scroll-to-top', function(e) {
|
||||
var $anchor = $(this);
|
||||
$('html, body').stop().animate({
|
||||
scrollTop: ($($anchor.attr('href')).offset().top)
|
||||
}, 1000, 'easeInOutExpo');
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
|
||||
$(".online-status").each(function(){
|
||||
const onlineStatusID = "#" + $(this).attr('id');
|
||||
$.get( "/user/status?pkey=" + encodeURIComponent($(this).attr('data-pkey')), function( data ) {
|
||||
console.log(onlineStatusID + " " + data)
|
||||
if(data === true) {
|
||||
$(onlineStatusID).html('<i class="fas fa-link text-success"></i>');
|
||||
} else {
|
||||
$(onlineStatusID).html('<i class="fas fa-unlink"></i>');
|
||||
}
|
||||
});
|
||||
});
|
||||
$(function() {
|
||||
$('select.device-selector').change(function() {
|
||||
this.form.submit();
|
||||
});
|
||||
});
|
||||
$('[data-toggle=confirmation]').confirmation({
|
||||
rootSelector: '[data-toggle=confirmation]',
|
||||
// other options
|
||||
});
|
||||
})(jQuery); // End of use strict
|
||||
|
||||
|
13
assets/js/jquery-ui.min.js
vendored
13
assets/js/jquery-ui.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,73 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/jquery-ui.min.css">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/bootstrap-tokenfield.min.css">
|
||||
<link rel="stylesheet" href="/css/tokenfield-typeahead.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>Create new clients</h1>
|
||||
<h2>Enter valid user email addresses to quickly create new accounts.</h2>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputEmail">Email Addresses</label>
|
||||
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.FormData.Emails}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputIdentifier">Client Friendly Name (will be added as suffix to the name of the user)</label>
|
||||
<input type="text" name="identifier" class="form-control" id="inputIdentifier" value="{{.FormData.Identifier}}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/bootstrap-tokenfield.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
<script>$('#inputEmail').on('tokenfield:createdtoken', function (e) {
|
||||
// Über-simplistic e-mail validation
|
||||
var re = /\S+@\S+\.\S+/
|
||||
var valid = re.test(e.attrs.value)
|
||||
if (!valid) {
|
||||
$(e.relatedTarget).addClass('invalid')
|
||||
}
|
||||
}).on('tokenfield:createtoken', function (e) {
|
||||
var existingTokens = $(this).tokenfield('getTokens');
|
||||
$.each(existingTokens, function(index, token) {
|
||||
if (token.value === e.attrs.value)
|
||||
e.preventDefault();
|
||||
});
|
||||
}).tokenfield({
|
||||
autocomplete: {
|
||||
source: [{{range $i, $u :=.Users}}{{if ne $i 0}},{{end}}'{{$u.Email}}'{{end}}],
|
||||
delay: 100
|
||||
},
|
||||
inputType: 'email',
|
||||
createTokensOnBlur: true,
|
||||
showAutocompleteOnFocus: false
|
||||
})</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,221 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<!-- server mode -->
|
||||
{{if eq .Device.Type "server"}}
|
||||
{{if .Peer.IsNew}}
|
||||
<h1>Create a new client</h1>
|
||||
{{else}}
|
||||
<h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1>
|
||||
{{end}}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="uid" value="{{.Peer.UID}}">
|
||||
<input type="hidden" name="devicetype" value="{{.Device.Type}}">
|
||||
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
|
||||
<input type="hidden" name="endpoint" value="{{.Peer.Endpoint}}">
|
||||
{{if .EditableKeys}}
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PrivateKey">Private Key</label>
|
||||
<input type="text" name="privkey" class="form-control" id="server_PrivateKey" value="{{.Peer.PrivateKey}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PresharedKey">Preshared Key</label>
|
||||
<input type="text" name="presharedkey" class="form-control" id="server_PresharedKey" value="{{.Peer.PresharedKey}}">
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
|
||||
<input type="hidden" name="presharedkey" value="{{.Peer.PresharedKey}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_ro_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" readonly class="form-control" id="server_ro_PublicKey" value="{{.Peer.PublicKey}}">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_Identifier">Client Friendly Name</label>
|
||||
<input type="text" name="identifier" class="form-control" id="server_Identifier" value="{{.Peer.Identifier}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_Email">Client Email Address</label>
|
||||
<input type="email" name="mail" class="form-control" id="server_Email" value="{{.Peer.Email}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_IP">Client IP Address</label>
|
||||
<input type="text" name="ip" class="form-control" id="server_IP" value="{{.Peer.IPsStr}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12 global-config">
|
||||
<label for="server_AllowedIP">Allowed IPs</label>
|
||||
<input type="text" name="allowedip" class="form-control" id="server_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_AllowedIPSrv">Extra Allowed IPs (Server sided)</label>
|
||||
<input type="text" name="allowedipSrv" class="form-control" id="server_AllowedIPSrv" value="{{.Peer.AllowedIPsSrvStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12 global-config">
|
||||
<label for="server_DNS">Client DNS Servers</label>
|
||||
<input type="text" name="dns" class="form-control" id="server_DNS" value="{{.Peer.DNSStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6 global-config">
|
||||
<label for="server_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
|
||||
<input type="number" name="keepalive" class="form-control" id="server_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6 global-config">
|
||||
<label for="server_MTU">Client MTU (0 = default)</label>
|
||||
<input type="number" name="mtu" class="form-control" id="server_MTU" placeholder="" value="{{.Peer.Mtu}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
|
||||
<label class="custom-control-label" for="server_Disabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="ignoreglobalsettings" type="checkbox" value="true" id="server_IgnoreGlobalSettings" {{if .Peer.IgnoreGlobalSettings}}checked{{end}}>
|
||||
<label class="custom-control-label" for="server_IgnoreGlobalSettings">
|
||||
Ignore global settings (<span class="text-blue">g</span>)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="expires_at">Expires At</label>
|
||||
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
{{end}}
|
||||
|
||||
<!-- client mode -->
|
||||
{{if eq .Device.Type "client"}}
|
||||
{{if .Peer.IsNew}}
|
||||
<h1>Create a new remote endpoint</h1>
|
||||
{{else}}
|
||||
<h1>Edit remote endpoint: <strong>{{.Peer.Identifier}}</strong></h1>
|
||||
{{end}}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="uid" value="{{.Peer.UID}}">
|
||||
<input type="hidden" name="mail" value="{{.AdminEmail}}">
|
||||
<input type="hidden" name="devicetype" value="{{.Device.Type}}">
|
||||
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
|
||||
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_Identifier">Endpoint Friendly Name</label>
|
||||
<input type="text" name="identifier" class="form-control" id="client_Identifier" value="{{.Peer.Identifier}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_Endpoint">Endpoint Address</label>
|
||||
<input type="text" name="endpoint" class="form-control" id="client_Endpoint" value="{{.Peer.Endpoint}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_PublicKey">Endpoint Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="client_PublicKey" value="{{.Peer.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PresharedKey">Preshared Key</label>
|
||||
<input type="text" name="presharedkey" class="form-control" id="client_PresharedKey" value="{{.Peer.PresharedKey}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_AllowedIP">Allowed IPs</label>
|
||||
<input type="text" name="allowedip" class="form-control" id="client_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="client_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
|
||||
<input type="number" name="keepalive" class="form-control" id="client_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="client_IP">Ping-Check IP Address</label>
|
||||
<input type="text" name="ip" class="form-control" id="client_IP" value="{{.Peer.IPsStr}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="client_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
|
||||
<label class="custom-control-label" for="client_Disabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="expires_at">Expires At</label>
|
||||
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,263 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5 main-app">
|
||||
<h1>Edit interface <strong>{{.Device.DeviceName}}</strong></h1>
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{if eq .Device.Type "server"}}active{{end}}" data-toggle="tab" href="#server">Server Mode</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{if eq .Device.Type "client"}}active{{end}}" data-toggle="tab" href="#client">Client Mode</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div id="configContent" class="tab-content">
|
||||
<!-- server mode -->
|
||||
<div class="tab-pane fade {{if eq .Device.Type "server"}}active show{{end}}" id="server">
|
||||
<form method="post" enctype="multipart/form-data" name="server">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
|
||||
<input type="hidden" name="devicetype" value="server">
|
||||
<h3>Server's interface configuration</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_DisplayName">Display Name</label>
|
||||
<input type="text" name="displayname" class="form-control" id="server_DisplayName" value="{{.Device.DisplayName}}">
|
||||
</div>
|
||||
</div>
|
||||
{{if .EditableKeys}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PrivateKey">Private Key</label>
|
||||
<input type="text" name="privkey" class="form-control" id="server_PrivateKey" value="{{.Device.PrivateKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Device.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_ro_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" readonly class="form-control" id="server_ro_PublicKey" value="{{.Device.PublicKey}}">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-6">
|
||||
<label for="server_ListenPort">Listen port</label>
|
||||
<input type="number" name="port" class="form-control" id="server_ListenPort" placeholder="51820" value="{{.Device.ListenPort}}" required>
|
||||
</div>
|
||||
<div class="form-group required col-md-6">
|
||||
<label for="server_IPs">Server IP address</label>
|
||||
<input type="text" name="ip" class="form-control" id="server_IPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Client's global configuration (<span class="text-blue">g</span>)</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicEndpoint">Public Endpoint for Clients</label>
|
||||
<input type="text" name="endpoint" class="form-control" id="server_PublicEndpoint" placeholder="vpn.company.com:51820" value="{{.Device.DefaultEndpoint}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_DNS">DNS Servers</label>
|
||||
<input type="text" name="dns" class="form-control" id="server_DNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_AllowedIP">Default allowed IPs</label>
|
||||
<input type="text" name="allowedip" class="form-control" id="server_AllowedIP" placeholder="10.6.6.0/24" value="{{.Device.DefaultAllowedIPsStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_MTU">MTU (also used for the server interface, 0 = default)</label>
|
||||
<input type="number" name="mtu" class="form-control" id="server_MTU" placeholder="" value="{{.Device.Mtu}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
|
||||
<input type="number" name="keepalive" class="form-control" id="server_PersistentKeepalive" placeholder="16" value="{{.Device.DefaultPersistentKeepalive}}">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Interface configuration hooks</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PreUp">Pre Up</label>
|
||||
<input type="text" name="preup" class="form-control" id="server_PreUp" value="{{.Device.PreUp}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PostUp">Post Up</label>
|
||||
<input type="text" name="postup" class="form-control" id="server_PostUp" value="{{.Device.PostUp}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PreDown">Pre Down</label>
|
||||
<input type="text" name="predown" class="form-control" id="server_PreDown" value="{{.Device.PreDown}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PostDown">Post Down</label>
|
||||
<input type="text" name="postdown" class="form-control" id="server_PostDown" value="{{.Device.PostDown}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="#" class="advanced-settings btn btn-link collapsed" data-toggle="collapse" data-target="#collapseAdvancedServer" aria-expanded="false" aria-controls="collapseAdvancedServer">
|
||||
Advanced Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="collapseAdvancedServer" class="collapse" aria-labelledby="collapseAdvancedServer">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_FirewallMark">Firewall Mark (0 = default or off)</label>
|
||||
<input type="number" name="firewallmark" class="form-control" id="server_FirewallMark" placeholder="" value="{{.Device.FirewallMark}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_RoutingTable">Routing Table (empty = default or auto)</label>
|
||||
<input type="text" name="routingtable" class="form-control" id="server_RoutingTable" placeholder="auto" value="{{.Device.RoutingTable}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="saveconfig" type="checkbox" value="true" id="server_SaveConfig" {{if .Peer.SaveConfig}}checked{{end}}>
|
||||
<label class="custom-control-label" for="server_SaveConfig">
|
||||
Save Configuration (if interface was edited via WireGuard configuration tool)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
<a href="/admin/device/applyglobals" class="btn btn-dark float-right">Apply Global Settings (<span class="text-blue">g</span>) to clients</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- client mode -->
|
||||
<div class="tab-pane fade {{if eq .Device.Type "client"}}active show{{end}}" id="client">
|
||||
<form method="post" enctype="multipart/form-data" name="client">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
|
||||
<input type="hidden" name="devicetype" value="client">
|
||||
<h3>Client's interface configuration</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_DisplayName">Display Name</label>
|
||||
<input type="text" name="displayname" class="form-control" id="client_DisplayName" value="{{.Device.DisplayName}}">
|
||||
</div>
|
||||
</div>
|
||||
{{if .EditableKeys}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_PrivateKey">Private Key</label>
|
||||
<input type="text" name="privkey" class="form-control" id="client_PrivateKey" value="{{.Device.PrivateKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="client_PublicKey" value="{{.Device.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_ro_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" readonly class="form-control" id="client_ro_PublicKey" value="{{.Device.PublicKey}}">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-6">
|
||||
<label for="client_IPs">Client IP address</label>
|
||||
<input type="text" name="ip" class="form-control" id="client_IPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}" required>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="client_DNS">DNS Servers</label>
|
||||
<input type="text" name="dns" class="form-control" id="client_DNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_MTU">MTU (0 = default)</label>
|
||||
<input type="number" name="mtu" class="form-control" id="client_MTU" placeholder="" value="{{.Device.Mtu}}">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_FirewallMark">Firewall Mark (0 = default or off)</label>
|
||||
<input type="number" name="firewallmark" class="form-control" id="client_FirewallMark" placeholder="" value="{{.Device.FirewallMark}}">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_RoutingTable">Routing Table (empty = default or auto)</label>
|
||||
<input type="text" name="routingtable" class="form-control" id="client_RoutingTable" placeholder="auto" value="{{.Device.RoutingTable}}">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Interface configuration hooks</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PreUp">Pre Up</label>
|
||||
<input type="text" name="preup" class="form-control" id="client_PreUp" value="{{.Device.PreUp}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PostUp">Post Up</label>
|
||||
<input type="text" name="postup" class="form-control" id="client_PostUp" value="{{.Device.PostUp}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PreDown">Pre Down</label>
|
||||
<input type="text" name="predown" class="form-control" id="client_PreDown" value="{{.Device.PreDown}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PostDown">Post Down</label>
|
||||
<input type="text" name="postdown" class="form-control" id="client_PostDown" value="{{.Device.PostDown}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,95 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Users</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
{{if eq .User.CreatedAt .Epoch}}
|
||||
<h1>Create a new user</h1>
|
||||
{{else}}
|
||||
<h1>Edit user <strong>{{.User.Email}}</strong></h1>
|
||||
{{end}}
|
||||
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
{{if eq .User.CreatedAt .Epoch}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputEmail">Email</label>
|
||||
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.User.Email}}" required>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="email" value="{{.User.Email}}">
|
||||
{{end}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputFirstname">Firstname</label>
|
||||
<input type="text" name="firstname" class="form-control" id="inputFirstname" value="{{.User.Firstname}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputLastname">Lastname</label>
|
||||
<input type="text" name="lastname" class="form-control" id="inputLastname" value="{{.User.Lastname}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="inputPhone">Phone</label>
|
||||
<input type="text" name="phone" class="form-control" id="inputPhone" value="{{.User.Phone}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12 {{if eq .User.CreatedAt .Epoch}}required{{end}}">
|
||||
<label for="inputPassword">Password</label>
|
||||
<input type="password" name="password" class="form-control" id="inputPassword" {{if eq .User.CreatedAt .Epoch}}required{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isadmin" type="checkbox" value="true" id="inputAdmin" {{if .User.IsAdmin}}checked{{end}}>
|
||||
<label class="custom-control-label" for="inputAdmin">
|
||||
Administrator
|
||||
</label>
|
||||
</div>
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="inputDisabled" {{if .User.DeletedAt.Valid}}checked{{end}}>
|
||||
<label class="custom-control-label" for="inputDisabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin/users/" class="btn btn-secondary">Cancel</a>
|
||||
{{if eq $.Session.IsAdmin true}}
|
||||
{{if eq .User.Source "db"}}
|
||||
<a href="/admin/users/delete?pkey={{.User.Email}}" data-toggle="confirmation" data-title="Really delete user and associated peers?" title="Delete user and associated peers" class="btn btn-danger float-right">Delete</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,278 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN Administration</h1>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="mr-auto">Interface status for <strong>{{.Device.DeviceName}}</strong> {{if eq $.Device.Type "server"}}(server mode){{end}}{{if eq $.Device.Type "client"}}(client mode){{end}}</span>
|
||||
<a href="/admin/device/write?dev={{.Device.DeviceName}}" title="Write interface configuration"><i class="fas fa-save"></i></a>
|
||||
|
||||
<a href="/admin/device/download?dev={{.Device.DeviceName}}" title="Download interface configuration"><i class="fas fa-download"></i></a>
|
||||
|
||||
<a href="/admin/device/edit?dev={{.Device.DeviceName}}" title="Edit interface settings"><i class="fas fa-cog"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>{{.Device.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Public Endpoint:</td>
|
||||
<td>{{.Device.DefaultEndpoint}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Listening Port:</td>
|
||||
<td>{{.Device.ListenPort}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled Peers:</td>
|
||||
<td>{{len .Device.Interface.Peers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Peers:</td>
|
||||
<td>{{.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IP Address:</td>
|
||||
<td>{{.Device.IPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default allowed IP's:</td>
|
||||
<td>{{.Device.DefaultAllowedIPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default DNS servers:</td>
|
||||
<td>{{.Device.DNSStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default MTU:</td>
|
||||
<td>{{.Device.Mtu}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default Keepalive Interval:</td>
|
||||
<td>{{.Device.DefaultPersistentKeepalive}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>{{.Device.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled Endpoints:</td>
|
||||
<td>{{len .Device.Interface.Peers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Endpoints:</td>
|
||||
<td>{{.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IP Address:</td>
|
||||
<td>{{.Device.IPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DNS servers:</td>
|
||||
<td>{{.Device.DNSStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default MTU:</td>
|
||||
<td>{{.Device.Mtu}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-8 col-12">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<h2 class="mt-2">Current VPN Peers</h2>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<h2 class="mt-2">Current VPN Endpoints</h2>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-sm-4 col-12 text-right">
|
||||
<a href="/admin/peer/emailall" data-toggle="confirmation" data-title="Send mail to all peers?" title="Send mail to all peers" class="btn btn-light"><i class="fa fa-fw fa-paper-plane"></i></a>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-users"></i></a>
|
||||
{{end}}
|
||||
<a href="/admin/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
|
||||
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "peers" "id"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "peers" "pubKey"}}"></i></a></th>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "peers" "mail"}}"></i></a></th>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "peers" "ip"}}"></i></a></th>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<th scope="col"><a href="?sort=endpoint">Endpoint <i class="fa fa-fw {{.Session.GetSortIcon "peers" "endpoint"}}"></i></a></th>
|
||||
{{end}}
|
||||
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "peers" "handshake"}}"></i></a></th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $p :=.Peers}}
|
||||
{{$peerUser:=(userForEmail $.Users $p.Email)}}
|
||||
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
|
||||
<th scope="row" class="list-image-cell">
|
||||
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
|
||||
<!-- online check -->
|
||||
<span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
|
||||
</th>
|
||||
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
|
||||
<td>{{$p.PublicKey}}</td>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<td>{{$p.Email}}</td>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<td>{{$p.IPsStr}}</td>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<td>{{$p.Endpoint}}</td>
|
||||
{{end}}
|
||||
<td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</span></td>
|
||||
<td>
|
||||
{{if eq $.Session.IsAdmin true}}
|
||||
<a href="/admin/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hiddenRow">
|
||||
<td colspan="7" class="hiddenCell" style="white-space:nowrap">
|
||||
<div class="collapse" id="{{$p.UID}}" data-parent="#userTable">
|
||||
<div class="row collapsedRow">
|
||||
<div class="col-md-6 leftBorder">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
|
||||
</li>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t3{{$p.UID}}">Danger Zone</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="tabContent{{$p.UID}}">
|
||||
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
|
||||
<h4>User details</h4>
|
||||
{{if not $peerUser}}
|
||||
<p>No user information available...</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li>Firstname: {{$peerUser.Firstname}}</li>
|
||||
<li>Lastname: {{$peerUser.Lastname}}</li>
|
||||
<li>Phone: {{$peerUser.Phone}}</li>
|
||||
<li>Mail: {{$peerUser.Email}}</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
<h4>Connection / Traffic</h4>
|
||||
{{if not $p.Peer}}
|
||||
<p>No Traffic data available...</p>
|
||||
{{else}}
|
||||
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-network-wired" title="Last Endpoint"></i> {{$p.Peer.Endpoint}}{{end}}</p>
|
||||
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down" title="Download"></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up" title="Upload"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div id="t2{{$p.UID}}" class="tab-pane fade">
|
||||
<pre>{{$p.Config}}</pre>
|
||||
</div>
|
||||
{{end}}
|
||||
<div id="t3{{$p.UID}}" class="tab-pane fade">
|
||||
<a href="/admin/peer/delete?pkey={{$p.PublicKey}}" class="btn btn-danger" title="Delete peer">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<img class="list-image-large" loading="lazy" alt="Configuration QR Code" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if $p.DeactivatedAt}}
|
||||
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
|
||||
{{end}}
|
||||
{{if $p.WillExpire}}
|
||||
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Peer will expire on {{ formatDate $p.ExpiresAt}}</div>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div class="pull-right-lg mt-lg-5 mt-md-3">
|
||||
<a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
|
||||
<a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Currently listed peers: <strong>{{len .Peers}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Users</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN Users</h1>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-10 col-12">
|
||||
<h2 class="mt-2">All Users</h2>
|
||||
</div>
|
||||
<div class="col-sm-2 col-12 text-right">
|
||||
<a href="/admin/users/create" title="Add a user" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><a href="?sort=email">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "users" "email"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=lastname">Lastname <i class="fa fa-fw {{.Session.GetSortIcon "users" "lastname"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=firstname">Firstname <i class="fa fa-fw {{.Session.GetSortIcon "users" "firstname"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=source">Source <i class="fa fa-fw {{.Session.GetSortIcon "users" "source"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=admin">Is Admin <i class="fa fa-fw {{.Session.GetSortIcon "users" "admin"}}"></i></a></th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $u :=.Users}}
|
||||
<tr id="user-pos-{{$i}}" {{if $u.DeletedAt.Valid}}class="disabled-peer"{{end}}>
|
||||
<td>{{$u.Email}}</td>
|
||||
<td>{{$u.Lastname}}</td>
|
||||
<td>{{$u.Firstname}}</td>
|
||||
<td>{{$u.Source}}</td>
|
||||
<td>{{if $u.IsAdmin}}True{{else}}False{{end}}</td>
|
||||
<td>
|
||||
{{if eq $.Session.IsAdmin true}}
|
||||
{{if eq $u.Source "db"}}
|
||||
<a href="/admin/users/edit?pkey={{$u.Email}}" title="Edit user"><i class="fas fa-cog"></i></a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Currently listed users: <strong>{{len .Users}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,187 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="format-detection" content="date=no" />
|
||||
<meta name="format-detection" content="address=no" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Muli:400,400i,700,700i" rel="stylesheet" />
|
||||
<!--<![endif]-->
|
||||
<title>Email Template</title>
|
||||
<!--[if gte mso 9]>
|
||||
<style type="text/css" media="all">
|
||||
sup { font-size: 100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
|
||||
<style type="text/css" media="screen">
|
||||
/* Linked Styles */
|
||||
body { padding:0 !important; margin:0 !important; display:block !important; min-width:100% !important; width:100% !important; background: #ffffff; -webkit-text-size-adjust:none }
|
||||
a { color: #000000; text-decoration:none }
|
||||
p { padding:0 !important; margin:0 !important }
|
||||
img { -ms-interpolation-mode: bicubic; /* Allow smoother rendering of resized image in Internet Explorer */ }
|
||||
.mcnPreviewText { display: none !important; }
|
||||
|
||||
|
||||
/* Mobile styles */
|
||||
@media only screen and (max-device-width: 480px), only screen and (max-width: 480px) {
|
||||
.mobile-shell { width: 100% !important; min-width: 100% !important; }
|
||||
.bg { background-size: 100% auto !important; -webkit-background-size: 100% auto !important; }
|
||||
|
||||
.text-header,
|
||||
.m-center { text-align: center !important; }
|
||||
|
||||
.center { margin: 0 auto !important; }
|
||||
.container { padding: 20px 10px !important }
|
||||
|
||||
.td { width: 100% !important; min-width: 100% !important; }
|
||||
|
||||
.m-br-15 { height: 15px !important; }
|
||||
.p30-15 { padding: 30px 15px !important; }
|
||||
|
||||
.m-td,
|
||||
.m-hide { display: none !important; width: 0 !important; height: 0 !important; font-size: 0 !important; line-height: 0 !important; min-height: 0 !important; }
|
||||
|
||||
.m-block { display: block !important; }
|
||||
|
||||
.fluid-img img { width: 100% !important; max-width: 100% !important; height: auto !important; }
|
||||
|
||||
.column,
|
||||
.column-top,
|
||||
.column-empty,
|
||||
.column-empty2,
|
||||
.column-dir-top { float: left !important; width: 100% !important; display: block !important; }
|
||||
|
||||
.column-empty { padding-bottom: 10px !important; }
|
||||
.column-empty2 { padding-bottom: 30px !important; }
|
||||
|
||||
.content-spacing { width: 15px !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="body" style="padding:0 !important; margin:0 !important; display:block !important; min-width:100% !important; width:100% !important; background:#000000; -webkit-text-size-adjust:none;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#000000">
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<table width="650" border="0" cellspacing="0" cellpadding="0" class="mobile-shell">
|
||||
<tr>
|
||||
<td class="td container" style="width:650px; min-width:650px; font-size:0pt; line-height:0pt; margin:0; font-weight:normal; padding:55px 0px;">
|
||||
|
||||
<!-- Article / Image On The Left - Copy On The Right -->
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="tbrr p30-15" style="padding: 60px 30px; border-radius:26px 26px 0px 0px;" bgcolor="#ffffff">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<th class="column-top" width="210" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="fluid-img" style="font-size:0pt; line-height:0pt; text-align:left;"><img src="cid:{{$.QrcodePngName}}" width="210" height="210" border="0" alt="" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
<th class="column-empty2" width="30" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;"></th>
|
||||
<th class="column-top" width="280" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
{{if $.User}}
|
||||
<td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello {{$.User.Firstname}} {{$.User.Lastname}}</td>
|
||||
{{else}}
|
||||
<td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text pb20" style="color:#000000; font-family:Arial,sans-serif; font-size:14px; line-height:26px; text-align:left; padding-bottom:20px;">You or your administrator probably requested this VPN configuration. Scan the Qrcode or open the attached configuration file ({{$.Peer.GetConfigFileName}}) in the WireGuard VPN client to establish a secure VPN connection.</td>
|
||||
</tr>
|
||||
</table>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- END Article / Image On The Left - Copy On The Right -->
|
||||
|
||||
<!-- Two Columns / Articles -->
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td style="padding-bottom: 10px;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#ffffff">
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="p30-15" style="padding: 50px 30px;">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="h3 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:25px; line-height:32px; text-align:left; padding-bottom:20px;">About WireGuard</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text pb20" style="color:#000000; font-family:Arial,sans-serif; font-size:14px; line-height:26px; text-align:left; padding-bottom:20px;">WireGuard is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN.</td>
|
||||
</tr>
|
||||
<!-- Button -->
|
||||
<tr>
|
||||
<td align="left">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="blue-button text-button" style="background:#000000; color:#c1cddc; font-family:'Muli', Arial,sans-serif; font-size:14px; line-height:18px; padding:12px 30px; text-align:center; border-radius:0px 22px 22px 22px; font-weight:bold;"><a href="https://www.wireguard.com/install" target="_blank" class="link-white" style="color:#ffffff; text-decoration:none;"><span class="link-white" style="color:#ffffff; text-decoration:none;">Download WireGuard VPN Client</span></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END Button -->
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- END Two Columns / Articles -->
|
||||
|
||||
<!-- Footer -->
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="p30-15 bbrr" style="padding: 50px 30px; border-radius:0px 0px 26px 26px;" bgcolor="#ffffff">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td class="text-footer1 pb10" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:16px; line-height:20px; text-align:center; padding-bottom:10px;">This mail was generated using WireGuard Portal.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-footer2" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:12px; line-height:26px; text-align:center;"><a href="{{$.PortalUrl}}" target="_blank" rel="noopener noreferrer" class="link" style="color:#000000; text-decoration:none;"><span class="link" style="color:#000000; text-decoration:none;">Visit WireGuard Portal</span></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- END Footer -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Error</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container">
|
||||
<div class="text-center mt-5">
|
||||
<div class="error mx-auto" data-text="{{.Data.Code}}">
|
||||
<p class="m-0">{{.Data.Code}}</p>
|
||||
</div>
|
||||
<p class="text-dark mb-5 lead">{{.Data.Message}}</p>
|
||||
<p class="text-black-50 mb-0">{{.Data.Details}}</p><a href="/">← Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,89 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Theme: https://bootswatch.com/lux/ -->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }}</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-2">
|
||||
<div class="page-header">
|
||||
<h1>{{ .Static.WebsiteTitle }}</h1>
|
||||
</div>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<p class="lead">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. </p>
|
||||
<h3 class="mt-3">More Information</h3>
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">WireGuard Installation</div>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Installation</h4>
|
||||
<p class="card-text">Installation instructions for client software can be found on the official WireGuard website.</p>
|
||||
<a href="https://www.wireguard.com/install/" title="WireGuard Installation" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Instructions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">About WireGuard</div>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">About</h4>
|
||||
<p class="card-text">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.</p>
|
||||
<a href="https://www.wireguard.com/" title="WireGuard" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">More details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">About WireGuard Portal</div>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">WireGuard Portal</h4>
|
||||
<p class="card-text">WireGuard Portal is a simple, web based configuration portal for WireGuard.</p>
|
||||
<a href="https://github.com/h44z/wg-portal/" title="WireGuard Portal" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">More details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="jumbotron jumbotron-home">
|
||||
<h2 class="display-5">VPN Profiles</h2>
|
||||
<p class="lead">You can access and download your personal VPN configurations via your Userprofile.</p>
|
||||
<hr class="my-4">
|
||||
<p>To find all your configured profiles click on the button below.</p>
|
||||
<p class="lead">
|
||||
<a href="/user/profile" class="btn btn-primary btn-lg" title="User-Profile">Open My Profile</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
|
||||
<div class="jumbotron jumbotron-home">
|
||||
<h2 class="display-5">Administration Area</h2>
|
||||
<p class="lead">In the administration area you can manage WireGuard peers and the server interface as well as users that are allowed to log in to the WireGuard Portal.</p>
|
||||
<hr class="my-4">
|
||||
<p>To find all your configured profiles click on the button below.</p>
|
||||
<p class="lead">
|
||||
<a href="/admin/" class="btn btn-primary btn-lg" title="WireGuard Administration">Open WireGuard Administration</a>
|
||||
<a href="/admin/users/" class="btn btn-primary btn-lg" title="User Administration">Open User Administration</a>
|
||||
</p>
|
||||
</div>
|
||||
{{end}}{{end}}
|
||||
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,66 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .static.WebsiteTitle }} - Login</title>
|
||||
<meta name="description" content="{{ .static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/fonts/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome5-overrides.min.css">
|
||||
<link rel="stylesheet" href="/css/signin.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar" aria-controls="topNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="/"><img src="{{$.static.WebsiteLogo}}" alt="{{$.static.CompanyName}}"/></a>
|
||||
<div id="topNavbar" class="navbar-collapse collapse">
|
||||
</div><!--/.navbar-collapse -->
|
||||
</nav>
|
||||
<div class="container mt-1">
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">Please sign in</div>
|
||||
<div class="card-body">
|
||||
<form class="form-signin" method="post" name="login">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">Username</label>
|
||||
<input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter username or email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputPassword">Password</label>
|
||||
<input type="password" name="password" class="form-control" id="inputPassword" placeholder="Password">
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary btn-block mt-5" type="submit">Sign in</button>
|
||||
|
||||
{{ if eq .error true }}
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
{{.message}}
|
||||
</div>
|
||||
{{end}}
|
||||
</form>
|
||||
|
||||
<div class="card o-hidden border-0 my-5">
|
||||
<div class="card-body p-0">
|
||||
<a href="/" class="btn btn-white btn-block text-primary btn-user">Go Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_flashes.html" .}}
|
||||
</div>
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,5 +0,0 @@
|
||||
{{range $flash := $.Alerts}}
|
||||
<div class="alert alert-{{$flash.Type}}" role="alert">
|
||||
{{$flash.Message}}
|
||||
</div>
|
||||
{{end}}
|
@ -1,5 +0,0 @@
|
||||
<footer class="page-footer mt-auto">
|
||||
<div class="container mt-3">
|
||||
<p class="text-muted">Copyright © {{ $.Static.CompanyName }} {{$.Static.Year}}, version {{$.Static.Version}} <a class="float-right scroll-to-top" href="#page-top"><i class="fas fa-angle-up"></i></a></p>
|
||||
</div>
|
||||
</footer>
|
@ -1,61 +0,0 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar" aria-controls="topNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="/"><img src="{{$.Static.WebsiteLogo}}" alt="{{$.Static.CompanyName}}"/></a>
|
||||
<div id="topNavbar" class="navbar-collapse collapse">
|
||||
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
||||
<li class="nav-spacer"></li>
|
||||
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
|
||||
{{with eq $.Route "/admin/"}}
|
||||
<form class="form-inline my-2 my-lg-0" method="get">
|
||||
<input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{index $.Session.Search "peers"}}">
|
||||
<button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{with eq $.Route "/admin/users/"}}
|
||||
<form class="form-inline my-2 my-lg-0" method="get">
|
||||
<input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{index $.Session.Search "users"}}">
|
||||
<button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}{{end}}
|
||||
</ul>
|
||||
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
|
||||
{{with startsWith $.Route "/admin/"}}
|
||||
<form class="form-inline my-2 my-lg-0" method="get">
|
||||
<div class="form-group mr-sm-2">
|
||||
<select name="device" id="inputDevice" class="form-control device-selector">
|
||||
{{range $d, $dn := $.DeviceNames}}
|
||||
<option value="{{$d}}" {{if eq $d $.Session.DeviceName}}selected{{end}}>{{$d}} {{if and (ne $dn "") (ne $d $dn)}}({{$dn}}){{end}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}{{end}}
|
||||
{{if eq $.Session.LoggedIn true}}
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="navbar-text dropdown-toggle" data-toggle="dropdown">{{$.Session.Firstname}} {{$.Session.Lastname}} <span class="caret"></span></a>
|
||||
<div class="dropdown-menu">
|
||||
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
|
||||
<a class="dropdown-item" href="/admin/"><i class="fas fa-cogs"></i> Administration</a>
|
||||
<a class="dropdown-item" href="/admin/users/"><i class="fas fa-users-cog"></i> User Management</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
{{end}}{{end}}
|
||||
<a class="dropdown-item" href="/user/profile"><i class="fas fa-user"></i> Profile</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="/auth/logout"><i class="fas fa-sign-out-alt"></i> Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a href="/auth/login" class="navbar-text"><i class="fas fa-sign-in-alt fa-sm fa-fw mr-2 text-gray-400"></i> Login</a></li>
|
||||
{{end}}
|
||||
</div><!--/.navbar-collapse -->
|
||||
</nav>
|
||||
{{if not $.Device.IsValid}}
|
||||
<div class="container">
|
||||
<div class="alert alert-danger">Warning: WireGuard Interface {{$.Device.DeviceName}} is not fully configured! Configurations may be incomplete and non functional!</div>
|
||||
</div>
|
||||
{{end}}
|
@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<!-- server mode -->
|
||||
<h1>Create a new client</h1>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="uid" value="{{.Peer.UID}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/user/profile" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,54 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<!-- server mode -->
|
||||
<h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="uid" value="{{.Peer.UID}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required disabled="disabled">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}disabled="disabled"{{end}} {{if .Peer.DeactivatedAt}}checked{{end}}>
|
||||
<label class="custom-control-label" for="server_Disabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/user/profile" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,135 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Profile</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN User-Portal</h1>
|
||||
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-8 col-12">
|
||||
<h2 class="mt-2">Your VPN Profiles</h2>
|
||||
</div>
|
||||
<div class="col-sm-4 col-12 text-right">
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<a href="/user/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
|
||||
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "id"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "pubKey"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "mail"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "ip"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th>
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<th scope="col"></th>
|
||||
{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $p :=.Peers}}
|
||||
{{$peerUser:=(userForEmail $.Users $p.Email)}}
|
||||
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
|
||||
<th scope="row" class="list-image-cell">
|
||||
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
|
||||
<!-- online check -->
|
||||
<span class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
|
||||
</th>
|
||||
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
|
||||
<td>{{$p.PublicKey}}</td>
|
||||
<td>{{$p.Email}}</td>
|
||||
<td>{{$p.IPsStr}}</td>
|
||||
<td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</span></td>
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<td>
|
||||
<a href="/user/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
<tr class="hiddenRow">
|
||||
<td colspan="6" class="hiddenCell" style="white-space:nowrap">
|
||||
<div class="collapse" id="{{$p.UID}}" data-parent="#userTable">
|
||||
<div class="row collapsedRow">
|
||||
<div class="col-md-6 leftBorder">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="tabContent{{$p.UID}}">
|
||||
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
|
||||
<h4>User details</h4>
|
||||
{{if not $peerUser}}
|
||||
<p>No user information available...</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li>Firstname: {{$peerUser.Firstname}}</li>
|
||||
<li>Lastname: {{$peerUser.Lastname}}</li>
|
||||
<li>Phone: {{$peerUser.Phone}}</li>
|
||||
<li>Mail: {{$peerUser.Email}}</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
<h4>Traffic</h4>
|
||||
{{if not $p.Peer}}
|
||||
<p>No Traffic data available...</p>
|
||||
{{else}}
|
||||
<p>{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down"></i></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="t2{{$p.UID}}" class="tab-pane fade">
|
||||
<pre>{{$p.Config}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if $p.DeactivatedAt}}
|
||||
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
|
||||
{{end}}
|
||||
{{if $p.WillExpire}}
|
||||
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Profile expires on {{ formatDate $p.ExpiresAt}}</div>
|
||||
{{end}}
|
||||
<div class="pull-right-lg mt-lg-5 mt-md-3">
|
||||
<a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
|
||||
<a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Currently listed peers: <strong>{{len .Peers}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,35 +0,0 @@
|
||||
// source taken from https://git.prolicht.digital/golib/healthcheck/-/blob/master/cmd/hc/main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// main checks the given URL, if the response is not 200, it will return with exit code 1
|
||||
// on success, exit code 0 will be returned
|
||||
func main() {
|
||||
os.Exit(checkWebEndpointFromArgs())
|
||||
}
|
||||
|
||||
func checkWebEndpointFromArgs() int {
|
||||
if len(os.Args) < 2 {
|
||||
return 1
|
||||
}
|
||||
if status := checkWebEndpoint(os.Args[1]); !status {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func checkWebEndpoint(url string) bool {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 2,
|
||||
}
|
||||
if resp, err := client.Get(url); err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
@ -2,103 +2,64 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.prolicht.digital/golib/healthcheck"
|
||||
"github.com/h44z/wg-portal/internal/server"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/adapters"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/ports/api/core"
|
||||
handlersV0 "github.com/h44z/wg-portal/internal/ports/api/v0/handlers"
|
||||
"github.com/sirupsen/logrus"
|
||||
evbus "github.com/vardius/message-bus"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_ = setupLogger(logrus.StandardLogger())
|
||||
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
logrus.Infof("Starting web portal...")
|
||||
|
||||
logrus.Infof("sysinfo: os=%s, arch=%s", runtime.GOOS, runtime.GOARCH)
|
||||
logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
|
||||
cfg, err := config.GetConfig()
|
||||
internal.AssertNoError(err)
|
||||
|
||||
// Context for clean shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
rawDb, err := adapters.NewDatabase(cfg.Database)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
// start health check service on port 11223
|
||||
healthcheck.New(healthcheck.ListenOn("127.0.0.1:11223")).StartWithContext(ctx)
|
||||
database, err := adapters.NewSqlRepository(rawDb)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
service := server.Server{}
|
||||
if err := service.Setup(ctx); err != nil {
|
||||
logrus.Fatalf("setup failed: %v", err)
|
||||
wireGuard := adapters.NewWireGuardRepository()
|
||||
|
||||
shouldExit, err := app.HandleProgramArgs(cfg, rawDb, wireGuard)
|
||||
switch {
|
||||
case shouldExit && err == nil:
|
||||
return
|
||||
case shouldExit && err != nil:
|
||||
logrus.Errorf("failed to process program args: %v", err)
|
||||
os.Exit(1)
|
||||
case !shouldExit:
|
||||
internal.AssertNoError(err)
|
||||
}
|
||||
|
||||
// Attach signal handlers to context
|
||||
go func() {
|
||||
osCall := <-c
|
||||
logrus.Tracef("received system call: %v", osCall)
|
||||
cancel() // cancel the context
|
||||
}()
|
||||
queueSize := 100
|
||||
eventBus := evbus.New(queueSize)
|
||||
|
||||
// Start main process in background
|
||||
go service.Run()
|
||||
backend, err := app.New(cfg, eventBus, database, wireGuard)
|
||||
internal.AssertNoError(err)
|
||||
backend.Users.StartBackgroundJobs(ctx)
|
||||
|
||||
<-ctx.Done() // Wait until the context gets canceled
|
||||
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
||||
|
||||
// Give goroutines some time to stop gracefully
|
||||
logrus.Info("stopping WireGuard Portal Server...")
|
||||
time.Sleep(2 * time.Second)
|
||||
webSrv, err := core.NewServer(cfg, apiFrontend)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
logrus.Infof("stopped WireGuard Portal Server...")
|
||||
logrus.Exit(0)
|
||||
}
|
||||
|
||||
func setupLogger(logger *logrus.Logger) error {
|
||||
// Check environment variables for logrus settings
|
||||
level, ok := os.LookupEnv("LOG_LEVEL")
|
||||
if !ok {
|
||||
level = "debug" // Default logrus level
|
||||
}
|
||||
|
||||
useJSON, ok := os.LookupEnv("LOG_JSON")
|
||||
if !ok {
|
||||
useJSON = "false" // Default use human readable logging
|
||||
}
|
||||
|
||||
useColor, ok := os.LookupEnv("LOG_COLOR")
|
||||
if !ok {
|
||||
useColor = "true"
|
||||
}
|
||||
|
||||
switch level {
|
||||
case "off":
|
||||
logger.SetOutput(io.Discard)
|
||||
case "info":
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
case "debug":
|
||||
logger.SetLevel(logrus.DebugLevel)
|
||||
case "trace":
|
||||
logger.SetLevel(logrus.TraceLevel)
|
||||
}
|
||||
|
||||
var formatter logrus.Formatter
|
||||
if useJSON == "false" {
|
||||
f := new(logrus.TextFormatter)
|
||||
f.TimestampFormat = "2006-01-02 15:04:05"
|
||||
f.FullTimestamp = true
|
||||
if useColor == "true" {
|
||||
f.ForceColors = true
|
||||
}
|
||||
formatter = f
|
||||
} else {
|
||||
f := new(logrus.JSONFormatter)
|
||||
f.TimestampFormat = "2006-01-02 15:04:05"
|
||||
formatter = f
|
||||
}
|
||||
|
||||
logger.SetFormatter(formatter)
|
||||
|
||||
return nil
|
||||
go webSrv.Run(ctx, cfg.Web.ListeningAddress)
|
||||
fmt.Println(backend) // TODO: Remove
|
||||
|
||||
// wait until context gets cancelled
|
||||
<-ctx.Done()
|
||||
|
||||
logrus.Infof("Stopped web portal")
|
||||
}
|
||||
|
12
efs.go
12
efs.go
@ -1,12 +0,0 @@
|
||||
package wg_portal
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed assets/tpl/*
|
||||
var Templates embed.FS
|
||||
|
||||
//go:embed assets/css/*
|
||||
//go:embed assets/fonts/*
|
||||
//go:embed assets/img/*
|
||||
//go:embed assets/js/*
|
||||
var Statics embed.FS
|
120
go.mod
120
go.mod
@ -1,41 +1,95 @@
|
||||
module github.com/h44z/wg-portal
|
||||
|
||||
go 1.16
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
git.prolicht.digital/golib/healthcheck v1.1.1
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect
|
||||
github.com/evanphx/json-patch v0.5.2
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/coreos/go-oidc/v3 v3.5.0
|
||||
github.com/gin-contrib/cors v1.4.0
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/go-ldap/ldap/v3 v3.4.3
|
||||
github.com/go-openapi/spec v0.20.6 // indirect
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.0
|
||||
github.com/go-test/deep v1.0.8 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/milosgajdos/tenus v0.0.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/swaggo/gin-swagger v1.4.3
|
||||
github.com/swaggo/swag v1.8.2
|
||||
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
|
||||
github.com/gin-gonic/gin v1.8.2
|
||||
github.com/go-ldap/ldap/v3 v3.4.4
|
||||
github.com/h44z/lightmigrate v1.0.0
|
||||
github.com/h44z/lightmigrate-mysql v0.0.0-20220114152421-d1fec9d056f1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/swaggo/swag v1.8.9
|
||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
|
||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
|
||||
github.com/xhit/go-simple-mail/v2 v2.11.0
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0
|
||||
gorm.io/driver/mysql v1.4.3
|
||||
gorm.io/driver/sqlite v1.4.3
|
||||
gorm.io/gorm v1.24.0
|
||||
github.com/vardius/message-bus v1.1.5
|
||||
github.com/vishvananda/netlink v1.1.0
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0
|
||||
golang.org/x/crypto v0.5.0
|
||||
golang.org/x/oauth2 v0.4.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gorm.io/driver/mysql v1.4.5
|
||||
gorm.io/driver/postgres v1.4.6
|
||||
gorm.io/driver/sqlite v1.4.4
|
||||
gorm.io/driver/sqlserver v1.4.2
|
||||
gorm.io/gorm v1.24.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dchest/uniuri v1.2.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.8 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-test/deep v1.0.8 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.2.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||
github.com/mdlayher/genetlink v1.3.1 // indirect
|
||||
github.com/mdlayher/netlink v1.7.1 // indirect
|
||||
github.com/mdlayher/socket v0.4.0 // indirect
|
||||
github.com/microsoft/go-mssqldb v0.19.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
github.com/ugorji/go/codec v1.2.8 // indirect
|
||||
github.com/vishvananda/netns v0.0.2 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
golang.org/x/tools v0.5.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
494
go.sum
494
go.sum
@ -1,39 +1,35 @@
|
||||
git.prolicht.digital/golib/healthcheck v1.1.1 h1:bdx0MuGqAq0PCooPpiuPXsr4/Ok+yfJwq8P9ITq2eLI=
|
||||
git.prolicht.digital/golib/healthcheck v1.1.1/go.mod h1:wEqVrqHJ8NsSx5qlFGUlw74wJ/wDSKaA34QoyvsEkdc=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bos-hieu/mongostore v0.0.2/go.mod h1:8AbbVmDEb0yqJsBrWxZIAZOxIfv/tsP8CDtdHduZHGg=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw=
|
||||
github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/docker/libcontainer v2.2.1+incompatible h1:++SbbkCw+X8vAd4j2gOCzZ2Nn7s2xFALTf7LZKmM1/0=
|
||||
github.com/docker/libcontainer v2.2.1+incompatible/go.mod h1:osvj61pYsqhNCMLGX31xr7klUBhHb/ZBuXS0o1Fvwbw=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
|
||||
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0=
|
||||
github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk=
|
||||
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
||||
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
|
||||
github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963/go.mod h1:4lkInX8nHSR62NSmhXM3xtPeMSyfiR58NaEz+om1lHM=
|
||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
||||
@ -41,62 +37,68 @@ github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NB
|
||||
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.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
|
||||
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.3 h1:JCKUtJPIcyOuG7ctGabLKMgIlKnGumD/iGjuWeEruDI=
|
||||
github.com/go-ldap/ldap/v3 v3.4.3/go.mod h1:7LdHfVt6iIOESVEe3Bs4Jp2sHEKgDeduAhgM1/f9qmo=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
|
||||
github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
|
||||
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU=
|
||||
github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
|
||||
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
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.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
|
||||
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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=
|
||||
@ -105,333 +107,228 @@ github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/h44z/lightmigrate v1.0.0 h1:wvkXvySwUTUuEMx0MAZaXzLa8vqspy0g5KwVfqLnpWQ=
|
||||
github.com/h44z/lightmigrate v1.0.0/go.mod h1:2QbrB1JaoGU+2kWOqf98jeULUSzJtdxovMYbdCwPyaE=
|
||||
github.com/h44z/lightmigrate-mysql v0.0.0-20220114152421-d1fec9d056f1 h1:HyKr9mclK5tvGlpipJTG6unzaJs8Jhh/WhcjpBJZM2s=
|
||||
github.com/h44z/lightmigrate-mysql v0.0.0-20220114152421-d1fec9d056f1/go.mod h1:xjqn0LXf6ly5GLm+6FDGi3+S20gDSExNYAm0k95rFzo=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8=
|
||||
github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
|
||||
github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
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/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mdlayher/genetlink v1.2.0 h1:4yrIkRV5Wfk1WfpWTcoOlGmsWgQj3OtQN9ZsbrE+XtU=
|
||||
github.com/mdlayher/genetlink v1.2.0/go.mod h1:ra5LDov2KrUCZJiAtEvXXZBxGMInICMXIwshlJ+qRxQ=
|
||||
github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
|
||||
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
|
||||
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
||||
github.com/mdlayher/socket v0.2.3 h1:XZA2X2TjdOwNoNPVPclRCURoX/hokBY8nkTmRZFEheM=
|
||||
github.com/mdlayher/socket v0.2.3/go.mod h1:bz12/FozYNH/VbvC3q7TRIK/Y6dH1kCKsXaUeXi/FmY=
|
||||
github.com/mdlayher/genetlink v1.3.1 h1:roBiPnual+eqtRkKX2Jb8UQN5ZPWnhDCGj/wR6Jlz2w=
|
||||
github.com/mdlayher/genetlink v1.3.1/go.mod h1:uaIPxkWmGk753VVIzDtROxQ8+T+dkHqOI0vB1NA9S/Q=
|
||||
github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg=
|
||||
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
|
||||
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
|
||||
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/microsoft/go-mssqldb v0.19.0 h1:LMRSgLcNMF8paPX14xlyQBmBH+jnFylPsYpVZf86eHM=
|
||||
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/milosgajdos/tenus v0.0.3 h1:jmaJzwaY1DUyYVD0lM4U+uvP2kkEg1VahDqRFxIkVBE=
|
||||
github.com/milosgajdos/tenus v0.0.3/go.mod h1:eIjx29vNeDOYWJuCnaHY2r4fq5egetV26ry3on7p8qY=
|
||||
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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
|
||||
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
|
||||
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
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/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
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/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/gin-swagger v1.4.3 h1:mHJz+yzJne0udgYnC5qlDf4e7KuxUbVNX2dhD/cw2rU=
|
||||
github.com/swaggo/gin-swagger v1.4.3/go.mod h1:hBg6tGeKJsUu/P79BH+WGUR8nq2LuGE0O160+s4iefo=
|
||||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||
github.com/swaggo/swag v1.8.2 h1:D4aBiVS2a65zhyk3WFqOUz7Rz0sOaUcgeErcid5uGL4=
|
||||
github.com/swaggo/swag v1.8.2/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
|
||||
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e h1:nt2877sKfojlHCTOBXbpWjBkuWKritFaGIfgQwbQUls=
|
||||
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e/go.mod h1:B4+Kq1u5FlULTjFSM707Q6e/cOHFv0z/6QRoxubDIQ8=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/swaggo/swag v1.8.9 h1:kHtaBe/Ob9AZzAANfcn5c6RyCke9gG9QpH0jky0I/sA=
|
||||
github.com/swaggo/swag v1.8.9/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY=
|
||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
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 v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
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/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0=
|
||||
github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca h1:lpvAjPK+PcxnbcB8H7axIb4fMNwjX9bE4DzwPjGg8aE=
|
||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca/go.mod h1:XXKxNbpoLihvvT7orUZbs/iZayg1n4ip7iJakJPAwA8=
|
||||
github.com/wader/gormstore/v2 v2.0.0/go.mod h1:3BgNKFxRdVo2E4pq3e/eiim8qRDZzaveaIcIvu2T8r0=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs=
|
||||
github.com/xhit/go-simple-mail/v2 v2.11.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.mongodb.org/mongo-driver v1.9.0/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
||||
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
||||
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/vishvananda/netns v0.0.2 h1:Cn05BRLm+iRP/DZxyVSsfVyrzgjDbwHwkVt38qvXnNI=
|
||||
github.com/vishvananda/netns v0.0.2/go.mod h1:yitZXdAVI+yPFSb4QUe+VW3vOVl4PZPNcBgbPxAtJxw=
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
|
||||
golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/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-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/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-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
|
||||
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b h1:9JncmKXcUwE918my+H6xmjBdhK2jM/UTUNXxhRG1BAk=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b/go.mod h1:yp4gl6zOlnDGOZeWeDfMwQcsdOIQnMdhuPx9mwwWBL4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c h1:Okh6a1xpnJslG9Mn84pId1Mn+Q8cvpo4HCeeFWHo0cA=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb h1:9aqVcYEDHmSNb0uOWukxV5lHV09WqiSiCuhEgWNETLY=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb/go.mod h1:mQqgjkW8GQQcJQsbBvK890TKqUK1DfKWkuBGbOkuMHQ=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
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/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@ -440,28 +337,31 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
||||
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.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.0.4/go.mod h1:MEgp8tk2n60cSBCq5iTcPDw3ns8Gs+zOva9EUhkknTs=
|
||||
gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k=
|
||||
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
||||
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
|
||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.5 h1:u1lytId4+o9dDaNcPCFzNv7h6wvmc92UjNk3z8enSBU=
|
||||
gorm.io/driver/mysql v1.4.5/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/driver/postgres v1.4.6 h1:1FPESNXqIKG5JmraaH2bfCVlMQ7paLoCreFxDtqzwdc=
|
||||
gorm.io/driver/postgres v1.4.6/go.mod h1:UJChCNLFKeBqQRE+HrkFUbKbq9idPXmTOk2u4Wok8S4=
|
||||
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
|
||||
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/driver/sqlserver v1.4.2 h1:nMtEeKqv2R/vv9FoHUFWfXfP6SskAgRar0TPlZV1stk=
|
||||
gorm.io/driver/sqlserver v1.4.2/go.mod h1:XHwBuB4Tlh7DqO0x7Ema8dmyWsQW7wi38VQOAFkrbXY=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
|
||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg=
|
||||
gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
|
527
internal/adapters/database.go
Normal file
527
internal/adapters/database.go
Normal file
@ -0,0 +1,527 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/lightmigrate"
|
||||
"github.com/h44z/lightmigrate-mysql/mysql"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
gormMySQL "gorm.io/driver/mysql"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/driver/sqlserver"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var sqlMigrationFs embed.FS
|
||||
var SchemaVersion uint64 = 1
|
||||
|
||||
func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||
var gormDb *gorm.DB
|
||||
var err error
|
||||
|
||||
switch cfg.Type {
|
||||
case config.DatabaseMySQL:
|
||||
gormDb, err = gorm.Open(gormMySQL.Open(cfg.DSN), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open MySQL database: %w", err)
|
||||
}
|
||||
|
||||
sqlDB, _ := gormDb.DB()
|
||||
sqlDB.SetConnMaxLifetime(time.Minute * 5)
|
||||
sqlDB.SetMaxIdleConns(2)
|
||||
sqlDB.SetMaxOpenConns(10)
|
||||
err = sqlDB.Ping() // This DOES open a connection if necessary. This makes sure the database is accessible
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to ping MySQL database: %w", err)
|
||||
}
|
||||
case config.DatabaseMsSQL:
|
||||
gormDb, err = gorm.Open(sqlserver.Open(cfg.DSN), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open sqlserver database: %w", err)
|
||||
}
|
||||
case config.DatabasePostgres:
|
||||
gormDb, err = gorm.Open(postgres.Open(cfg.DSN), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open Postgres database: %w", err)
|
||||
}
|
||||
case config.DatabaseSQLite:
|
||||
if _, err = os.Stat(filepath.Dir(cfg.DSN)); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(filepath.Dir(cfg.DSN), 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create database base directory: %w", err)
|
||||
}
|
||||
}
|
||||
gormDb, err = gorm.Open(sqlite.Open(cfg.DSN), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return gormDb, nil
|
||||
}
|
||||
|
||||
type SqlRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
|
||||
repo := &SqlRepo{
|
||||
db: db,
|
||||
}
|
||||
|
||||
err := repo.migrate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) migrate() error {
|
||||
// TODO: REMOVE
|
||||
logrus.Warnf("user migration: %v", r.db.AutoMigrate(&domain.User{}))
|
||||
logrus.Warnf("interface migration: %v", r.db.AutoMigrate(&domain.Interface{}))
|
||||
logrus.Warnf("peer migration: %v", r.db.AutoMigrate(&domain.Peer{}))
|
||||
// TODO: REMOVE THE ABOVE LINES
|
||||
|
||||
rawDb, err := r.db.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get raw db handle: %w", err)
|
||||
}
|
||||
|
||||
driver, err := mysql.NewDriver(rawDb, "migration_test_db", mysql.WithLocking(false)) // without locking, the mysql driver also works for sqlite =)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to setup driver: %w", err)
|
||||
}
|
||||
defer driver.Close()
|
||||
|
||||
source, err := lightmigrate.NewFsSource(sqlMigrationFs, "migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open migration source fs: %w", err)
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
migrator, err := lightmigrate.NewMigrator(source, driver, lightmigrate.WithVerboseLogging(true))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to setup migrator: %w", err)
|
||||
}
|
||||
|
||||
err = migrator.Migrate(SchemaVersion)
|
||||
if err != nil && !errors.Is(err, lightmigrate.ErrNoChange) {
|
||||
return fmt.Errorf("failed to migrate database schema: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// region interfaces
|
||||
|
||||
func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
|
||||
var in domain.Interface
|
||||
|
||||
err := r.db.WithContext(ctx).First(&in, id).Error
|
||||
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, domain.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &in, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) {
|
||||
in, err := r.GetInterface(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load interface: %w", err)
|
||||
}
|
||||
|
||||
peers, err := r.GetInterfacePeers(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load peers: %w", err)
|
||||
}
|
||||
|
||||
return in, peers, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
||||
var interfaces []domain.Interface
|
||||
|
||||
err := r.db.WithContext(ctx).Preload("Addresses").Find(&interfaces).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
|
||||
var users []domain.Interface
|
||||
|
||||
searchValue := "%" + strings.ToLower(search) + "%"
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("identifier LIKE ?", searchValue).
|
||||
Or("display_name LIKE ?", searchValue).
|
||||
Preload("Addresses").
|
||||
Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.Interface) (*domain.Interface, error)) error {
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
in, err := r.getOrCreateInterface(tx, id)
|
||||
if err != nil {
|
||||
return err // return any error will roll back
|
||||
}
|
||||
|
||||
in, err = updateFunc(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.upsertInterface(tx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return nil will commit the whole transaction
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) getOrCreateInterface(tx *gorm.DB, id domain.InterfaceIdentifier) (*domain.Interface, error) {
|
||||
var in domain.Interface
|
||||
|
||||
// interfaceDefaults will be applied to newly created interface records
|
||||
interfaceDefaults := domain.Interface{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Identifier: id,
|
||||
}
|
||||
|
||||
err := tx.Attrs(interfaceDefaults).FirstOrCreate(&in, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &in, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) upsertInterface(tx *gorm.DB, in *domain.Interface) error {
|
||||
err := tx.Save(in).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
err := r.db.WithContext(ctx).Where("interface_identifier = ?", id).Delete(&domain.Peer{}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.db.WithContext(ctx).Delete(&domain.Interface{}, id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
|
||||
var ips []struct {
|
||||
domain.Cidr
|
||||
InterfaceId domain.InterfaceIdentifier `gorm:"column:interface_identifier"`
|
||||
}
|
||||
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("interface_addresses").
|
||||
Joins("LEFT JOIN cidrs ON interface_addresses.cidr_addr = cidrs.addr AND interface_addresses.cidr_net_length = cidrs.net_len").
|
||||
Scan(&ips).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[domain.InterfaceIdentifier][]domain.Cidr)
|
||||
for _, ip := range ips {
|
||||
result[ip.InterfaceId] = append(result[ip.InterfaceId], ip.Cidr)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// endregion interfaces
|
||||
|
||||
// region peers
|
||||
|
||||
func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
||||
var peers []domain.Peer
|
||||
|
||||
err := r.db.WithContext(ctx).Where("interface_identifier = ?", id).Find(&peers).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) ([]domain.Peer, error) {
|
||||
var peers []domain.Peer
|
||||
|
||||
searchValue := "%" + strings.ToLower(search) + "%"
|
||||
err := r.db.WithContext(ctx).Where("interface_identifier = ?", id).
|
||||
Where("identifier LIKE ?", searchValue).
|
||||
Or("display_name LIKE ?", searchValue).
|
||||
Or("iface_address_str_v LIKE ?", searchValue).
|
||||
Find(&peers).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||
var peers []domain.Peer
|
||||
|
||||
err := r.db.WithContext(ctx).Where("user_identifier = ?", id).Find(&peers).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error) {
|
||||
var peers []domain.Peer
|
||||
|
||||
searchValue := "%" + strings.ToLower(search) + "%"
|
||||
err := r.db.WithContext(ctx).Where("user_identifier = ?", id).
|
||||
Where("identifier LIKE ?", searchValue).
|
||||
Or("display_name LIKE ?", searchValue).
|
||||
Or("iface_address_str_v LIKE ?", searchValue).
|
||||
Find(&peers).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error {
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
peer, err := r.getOrCreatePeer(tx, id)
|
||||
if err != nil {
|
||||
return err // return any error will roll back
|
||||
}
|
||||
|
||||
peer, err = updateFunc(peer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.upsertPeer(tx, peer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return nil will commit the whole transaction
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) getOrCreatePeer(tx *gorm.DB, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||
var peer domain.Peer
|
||||
|
||||
// interfaceDefaults will be applied to newly created interface records
|
||||
interfaceDefaults := domain.Peer{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Identifier: id,
|
||||
}
|
||||
|
||||
err := tx.Attrs(interfaceDefaults).FirstOrCreate(&peer, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &peer, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) upsertPeer(tx *gorm.DB, peer *domain.Peer) error {
|
||||
err := tx.Save(peer).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
|
||||
err := r.db.WithContext(ctx).Delete(&domain.Peer{}, id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion peers
|
||||
|
||||
// region users
|
||||
|
||||
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||
var user domain.User
|
||||
|
||||
err := r.db.WithContext(ctx).First(&user, id).Error
|
||||
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, domain.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
|
||||
err := r.db.WithContext(ctx).Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
|
||||
searchValue := "%" + strings.ToLower(search) + "%"
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("identifier LIKE ?", searchValue).
|
||||
Or("firstname LIKE ?", searchValue).
|
||||
Or("lastname LIKE ?", searchValue).
|
||||
Or("email LIKE ?", searchValue).
|
||||
Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error {
|
||||
userInfo := domain.GetUserInfo(ctx)
|
||||
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
user, err := r.getOrCreateUser(string(userInfo.Id), tx, id)
|
||||
if err != nil {
|
||||
return err // return any error will roll back
|
||||
}
|
||||
|
||||
user, err = updateFunc(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.UpdatedAt = time.Now()
|
||||
user.UpdatedBy = string(userInfo.Id)
|
||||
|
||||
err = r.upsertUser(tx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return nil will commit the whole transaction
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
|
||||
err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) getOrCreateUser(creator string, tx *gorm.DB, id domain.UserIdentifier) (*domain.User, error) {
|
||||
var user domain.User
|
||||
|
||||
// userDefaults will be applied to newly created user records
|
||||
userDefaults := domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: creator,
|
||||
UpdatedBy: creator,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Identifier: id,
|
||||
Source: domain.UserSourceDatabase,
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
err := tx.Attrs(userDefaults).FirstOrCreate(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) upsertUser(tx *gorm.DB, user *domain.User) error {
|
||||
err := tx.Save(user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion users
|
43
internal/adapters/database_integration_test.go
Normal file
43
internal/adapters/database_integration_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
//go:build integration
|
||||
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func tempSqliteDb(t *testing.T) *gorm.DB {
|
||||
|
||||
// github.com/mattn/go-sqlite3
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func Test_sqlRepo_migrate(t *testing.T) {
|
||||
db := tempSqliteDb(t)
|
||||
|
||||
r := SqlRepo{db: db}
|
||||
|
||||
err := r.migrate()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check result
|
||||
var sqlStatement []sql.NullString
|
||||
db.Raw("SELECT sql FROM sqlite_master").Find(&sqlStatement)
|
||||
fmt.Println("Table Schemas:")
|
||||
for _, stm := range sqlStatement {
|
||||
if stm.Valid {
|
||||
fmt.Println(stm.String)
|
||||
}
|
||||
}
|
||||
}
|
45
internal/adapters/filesystem.go
Normal file
45
internal/adapters/filesystem.go
Normal file
@ -0,0 +1,45 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type filesystemRepo struct {
|
||||
basePath string
|
||||
}
|
||||
|
||||
func NewFileSystemRepository(basePath string) (*filesystemRepo, error) {
|
||||
r := &filesystemRepo{basePath: basePath}
|
||||
|
||||
if err := os.MkdirAll(r.basePath, os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *filesystemRepo) WriteFile(_ context.Context, path string, contents io.Reader) error {
|
||||
filePath := filepath.Join(r.basePath, path)
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, contents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
150
internal/adapters/mailer.go
Normal file
150
internal/adapters/mailer.go
Normal file
@ -0,0 +1,150 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
)
|
||||
|
||||
type mailRepo struct {
|
||||
cfg *config.MailConfig
|
||||
}
|
||||
|
||||
func NewSmtpMailRepo(cfg *config.MailConfig) *mailRepo {
|
||||
return &mailRepo{cfg: cfg}
|
||||
}
|
||||
|
||||
// Send sends a mail.
|
||||
func (r *mailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error {
|
||||
if options == nil {
|
||||
options = &domain.MailOptions{}
|
||||
}
|
||||
r.setDefaultOptions(r.cfg.From, options)
|
||||
|
||||
if len(to) == 0 {
|
||||
return errors.New("missing email recipient")
|
||||
}
|
||||
|
||||
uniqueTo := r.uniqueAddresses(to)
|
||||
email := mail.NewMSG()
|
||||
email.SetFrom(r.cfg.From).
|
||||
AddTo(uniqueTo...).
|
||||
SetReplyTo(options.ReplyTo).
|
||||
SetSubject(subject).
|
||||
SetBody(mail.TextPlain, body)
|
||||
|
||||
if len(options.Cc) > 0 {
|
||||
// the underlying mail library does not allow the same address to appear in TO and CC... so filter entries that are already included
|
||||
// in the TO addresses
|
||||
cc := r.removeDuplicates(r.uniqueAddresses(options.Cc), uniqueTo)
|
||||
email.AddCc(cc...)
|
||||
}
|
||||
if len(options.Bcc) > 0 {
|
||||
// the underlying mail library does not allow the same address to appear in TO or CC and BCC... so filter entries that are already
|
||||
//included in the TO and CC addresses
|
||||
bcc := r.removeDuplicates(r.uniqueAddresses(options.Bcc), uniqueTo)
|
||||
bcc = r.removeDuplicates(bcc, options.Cc)
|
||||
|
||||
email.AddCc(r.uniqueAddresses(options.Bcc)...)
|
||||
}
|
||||
if options.HtmlBody != "" {
|
||||
email.AddAlternative(mail.TextHTML, options.HtmlBody)
|
||||
}
|
||||
|
||||
for _, attachment := range options.Attachments {
|
||||
attachmentData, err := ioutil.ReadAll(attachment.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read attachment data for %s: %w", attachment.Name, err)
|
||||
}
|
||||
|
||||
if attachment.Embedded {
|
||||
email.AddInlineData(attachmentData, attachment.Name, attachment.ContentType)
|
||||
} else {
|
||||
email.AddAttachmentData(attachmentData, attachment.Name, attachment.ContentType)
|
||||
}
|
||||
}
|
||||
|
||||
// Call Send and pass the client
|
||||
srv := r.getMailServer()
|
||||
client, err := srv.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||
}
|
||||
|
||||
err = email.Send(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mailRepo) setDefaultOptions(sender string, options *domain.MailOptions) {
|
||||
if options.ReplyTo == "" {
|
||||
options.ReplyTo = sender
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mailRepo) getMailServer() *mail.SMTPServer {
|
||||
srv := mail.NewSMTPClient()
|
||||
|
||||
srv.ConnectTimeout = 30 * time.Second
|
||||
srv.SendTimeout = 30 * time.Second
|
||||
srv.Host = r.cfg.Host
|
||||
srv.Port = r.cfg.Port
|
||||
srv.Username = r.cfg.Username
|
||||
srv.Password = r.cfg.Password
|
||||
|
||||
switch r.cfg.Encryption {
|
||||
case config.MailEncryptionTLS:
|
||||
srv.Encryption = mail.EncryptionSSLTLS
|
||||
case config.MailEncryptionStartTLS:
|
||||
srv.Encryption = mail.EncryptionSTARTTLS
|
||||
default: // MailEncryptionNone
|
||||
srv.Encryption = mail.EncryptionNone
|
||||
}
|
||||
srv.TLSConfig = &tls.Config{ServerName: srv.Host, InsecureSkipVerify: !r.cfg.CertValidation}
|
||||
switch r.cfg.AuthType {
|
||||
case config.MailAuthPlain:
|
||||
srv.Authentication = mail.AuthPlain
|
||||
case config.MailAuthLogin:
|
||||
srv.Authentication = mail.AuthLogin
|
||||
case config.MailAuthCramMD5:
|
||||
srv.Authentication = mail.AuthCRAMMD5
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
// uniqueAddresses removes duplicates in the given string slice
|
||||
func (r *mailRepo) uniqueAddresses(slice []string) []string {
|
||||
keys := make(map[string]struct{})
|
||||
uniqueSlice := make([]string, 0, len(slice))
|
||||
for _, entry := range slice {
|
||||
if _, exists := keys[entry]; !exists {
|
||||
keys[entry] = struct{}{}
|
||||
uniqueSlice = append(uniqueSlice, entry)
|
||||
}
|
||||
}
|
||||
return uniqueSlice
|
||||
}
|
||||
|
||||
func (r *mailRepo) removeDuplicates(slice []string, remove []string) []string {
|
||||
uniqueSlice := make([]string, 0, len(slice))
|
||||
|
||||
for _, i := range remove {
|
||||
for _, j := range slice {
|
||||
if i != j {
|
||||
uniqueSlice = append(uniqueSlice, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
return uniqueSlice
|
||||
}
|
1
internal/adapters/migrations/1_init.down.sql
Normal file
1
internal/adapters/migrations/1_init.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS test;
|
3
internal/adapters/migrations/1_init.up.sql
Normal file
3
internal/adapters/migrations/1_init.up.sql
Normal file
@ -0,0 +1,3 @@
|
||||
CREATE TABLE IF NOT EXISTS test (
|
||||
firstname VARCHAR(16)
|
||||
);
|
412
internal/adapters/wireguard.go
Normal file
412
internal/adapters/wireguard.go
Normal file
@ -0,0 +1,412 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.zx2c4.com/wireguard/wgctrl"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type wgRepo struct {
|
||||
wg lowlevel.WireGuardClient
|
||||
nl lowlevel.NetlinkClient
|
||||
}
|
||||
|
||||
func NewWireGuardRepository() *wgRepo {
|
||||
wg, err := wgctrl.New()
|
||||
if err != nil {
|
||||
panic("failed to init wgctrl: " + err.Error())
|
||||
}
|
||||
|
||||
nl := &lowlevel.NetlinkManager{}
|
||||
|
||||
repo := &wgRepo{
|
||||
wg: wg,
|
||||
nl: nl,
|
||||
}
|
||||
|
||||
return repo
|
||||
}
|
||||
|
||||
func (r *wgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||
devices, err := r.wg.Devices()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("device list error: %w", err)
|
||||
}
|
||||
|
||||
interfaces := make([]domain.PhysicalInterface, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
interfaceModel, err := r.convertWireGuardInterface(device)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("interface convert failed for %s: %w", device.Name, err)
|
||||
}
|
||||
interfaces = append(interfaces, interfaceModel)
|
||||
}
|
||||
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||
return r.getInterface(id)
|
||||
}
|
||||
|
||||
func (r *wgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
|
||||
device, err := r.wg.Device(string(deviceId))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("device error: %w", err)
|
||||
}
|
||||
|
||||
peers := make([]domain.PhysicalPeer, 0, len(device.Peers))
|
||||
for _, peer := range device.Peers {
|
||||
peerModel, err := r.convertWireGuardPeer(&peer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peer convert failed for %v: %w", peer.PublicKey, err)
|
||||
}
|
||||
peers = append(peers, peerModel)
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) GetPeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error) {
|
||||
return r.getPeer(deviceId, id)
|
||||
}
|
||||
|
||||
func (r *wgRepo) convertWireGuardInterface(device *wgtypes.Device) (domain.PhysicalInterface, error) {
|
||||
// read data from wgctrl interface
|
||||
|
||||
iface := domain.PhysicalInterface{
|
||||
Identifier: domain.InterfaceIdentifier(device.Name),
|
||||
KeyPair: domain.KeyPair{
|
||||
PrivateKey: device.PrivateKey.String(),
|
||||
PublicKey: device.PublicKey.String(),
|
||||
},
|
||||
ListenPort: device.ListenPort,
|
||||
Addresses: nil,
|
||||
Mtu: 0,
|
||||
FirewallMark: int32(device.FirewallMark),
|
||||
DeviceUp: false,
|
||||
ImportSource: "wgctrl",
|
||||
DeviceType: device.Type.String(),
|
||||
BytesUpload: 0,
|
||||
BytesDownload: 0,
|
||||
}
|
||||
|
||||
// read data from netlink interface
|
||||
|
||||
lowLevelInterface, err := r.nl.LinkByName(device.Name)
|
||||
if err != nil {
|
||||
return domain.PhysicalInterface{}, fmt.Errorf("netlink error for %s: %w", device.Name, err)
|
||||
}
|
||||
ipAddresses, err := r.nl.AddrList(lowLevelInterface)
|
||||
if err != nil {
|
||||
return domain.PhysicalInterface{}, fmt.Errorf("ip read error for %s: %w", device.Name, err)
|
||||
}
|
||||
|
||||
for _, addr := range ipAddresses {
|
||||
iface.Addresses = append(iface.Addresses, domain.CidrFromNetlinkAddr(addr))
|
||||
}
|
||||
iface.Mtu = lowLevelInterface.Attrs().MTU
|
||||
iface.DeviceUp = lowLevelInterface.Attrs().OperState == netlink.OperUnknown // wg only supports unknown
|
||||
if stats := lowLevelInterface.Attrs().Statistics; stats != nil {
|
||||
iface.BytesUpload = stats.TxBytes
|
||||
iface.BytesDownload = stats.RxBytes
|
||||
}
|
||||
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) convertWireGuardPeer(peer *wgtypes.Peer) (domain.PhysicalPeer, error) {
|
||||
peerModel := domain.PhysicalPeer{
|
||||
Identifier: domain.PeerIdentifier(peer.PublicKey.String()),
|
||||
Endpoint: "",
|
||||
AllowedIPs: nil,
|
||||
KeyPair: domain.KeyPair{
|
||||
PublicKey: peer.PublicKey.String(),
|
||||
},
|
||||
PresharedKey: "",
|
||||
PersistentKeepalive: int(peer.PersistentKeepaliveInterval.Seconds()),
|
||||
LastHandshake: peer.LastHandshakeTime,
|
||||
ProtocolVersion: peer.ProtocolVersion,
|
||||
BytesUpload: uint64(peer.ReceiveBytes),
|
||||
BytesDownload: uint64(peer.TransmitBytes),
|
||||
}
|
||||
|
||||
for _, addr := range peer.AllowedIPs {
|
||||
peerModel.AllowedIPs = append(peerModel.AllowedIPs, domain.CidrFromIpNet(addr))
|
||||
}
|
||||
if peer.Endpoint != nil {
|
||||
peerModel.Endpoint = peer.Endpoint.String()
|
||||
}
|
||||
if peer.PresharedKey != (wgtypes.Key{}) {
|
||||
peerModel.PresharedKey = domain.PreSharedKey(peer.PresharedKey.String())
|
||||
}
|
||||
|
||||
return peerModel, nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) SaveInterface(_ context.Context, id domain.InterfaceIdentifier, updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error)) error {
|
||||
physicalInterface, err := r.getOrCreateInterface(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if updateFunc != nil {
|
||||
physicalInterface, err = updateFunc(physicalInterface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.updateLowLevelInterface(physicalInterface); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.updateWireGuardInterface(physicalInterface); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||
device, err := r.getInterface(id)
|
||||
if err == nil {
|
||||
return device, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("device error: %w", err)
|
||||
}
|
||||
|
||||
// create new device
|
||||
if err := r.createLowLevelInterface(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
device, err = r.getInterface(id)
|
||||
return device, err
|
||||
}
|
||||
|
||||
func (r *wgRepo) getInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||
device, err := r.wg.Device(string(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pi, err := r.convertWireGuardInterface(device)
|
||||
return &pi, err
|
||||
}
|
||||
|
||||
func (r *wgRepo) createLowLevelInterface(id domain.InterfaceIdentifier) error {
|
||||
link := &netlink.GenericLink{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: string(id),
|
||||
},
|
||||
LinkType: "wireguard",
|
||||
}
|
||||
err := r.nl.LinkAdd(link)
|
||||
if err != nil {
|
||||
return fmt.Errorf("link add failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) updateLowLevelInterface(pi *domain.PhysicalInterface) error {
|
||||
link, err := r.nl.LinkByName(string(pi.Identifier))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pi.Mtu != 0 {
|
||||
if err := r.nl.LinkSetMTU(link, pi.Mtu); err != nil {
|
||||
return fmt.Errorf("mtu error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, addr := range pi.Addresses {
|
||||
var err error
|
||||
if i == 0 {
|
||||
err = r.nl.AddrReplace(link, addr.NetlinkAddr())
|
||||
} else {
|
||||
err = r.nl.AddrAdd(link, addr.NetlinkAddr())
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set ip %s: %w", addr.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update link state
|
||||
if pi.DeviceUp {
|
||||
if err := r.nl.LinkSetUp(link); err != nil {
|
||||
return fmt.Errorf("failed to bring up device: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := r.nl.LinkSetDown(link); err != nil {
|
||||
return fmt.Errorf("failed to bring down device: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
|
||||
pKey, err := wgtypes.NewKey(pi.KeyPair.GetPrivateKeyBytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fwMark *int
|
||||
if pi.FirewallMark != 0 {
|
||||
*fwMark = int(pi.FirewallMark)
|
||||
}
|
||||
err = r.wg.ConfigureDevice(string(pi.Identifier), wgtypes.Config{
|
||||
PrivateKey: &pKey,
|
||||
ListenPort: &pi.ListenPort,
|
||||
FirewallMark: fwMark,
|
||||
ReplacePeers: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
||||
if err := r.deleteLowLevelInterface(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) deleteLowLevelInterface(id domain.InterfaceIdentifier) error {
|
||||
link, err := r.nl.LinkByName(string(id))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to find low level interface: %w", err)
|
||||
}
|
||||
|
||||
err = r.nl.LinkDel(link)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete low level interface: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) SavePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier, updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error)) error {
|
||||
physicalPeer, err := r.getOrCreatePeer(deviceId, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
physicalPeer, err = updateFunc(physicalPeer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.updatePeer(deviceId, physicalPeer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error) {
|
||||
peer, err := r.getPeer(deviceId, id)
|
||||
if err == nil {
|
||||
return peer, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("peer error: %w", err)
|
||||
}
|
||||
|
||||
// create new peer
|
||||
err = r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{Peers: []wgtypes.PeerConfig{{
|
||||
PublicKey: id.ToPublicKey(),
|
||||
}}})
|
||||
|
||||
peer, err = r.getPeer(deviceId, id)
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) getPeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error) {
|
||||
if !id.IsPublicKey() {
|
||||
return nil, errors.New("invalid public key")
|
||||
}
|
||||
|
||||
device, err := r.wg.Device(string(deviceId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKey := id.ToPublicKey()
|
||||
for _, peer := range device.Peers {
|
||||
if peer.PublicKey != publicKey {
|
||||
continue
|
||||
}
|
||||
|
||||
peerModel, err := r.convertWireGuardPeer(&peer)
|
||||
return &peerModel, err
|
||||
}
|
||||
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (r *wgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.PhysicalPeer) error {
|
||||
cfg := wgtypes.PeerConfig{
|
||||
PublicKey: pp.GetPublicKey(),
|
||||
Remove: false,
|
||||
UpdateOnly: true,
|
||||
PresharedKey: pp.GetPresharedKey(),
|
||||
Endpoint: pp.GetEndpointAddress(),
|
||||
PersistentKeepaliveInterval: pp.GetPersistentKeepaliveTime(),
|
||||
ReplaceAllowedIPs: true,
|
||||
AllowedIPs: nil,
|
||||
}
|
||||
|
||||
ips, err := pp.GetAllowedIPs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.AllowedIPs = ips
|
||||
|
||||
err = r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
||||
if !id.IsPublicKey() {
|
||||
return errors.New("invalid public key")
|
||||
}
|
||||
|
||||
err := r.deletePeer(deviceId, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *wgRepo) deletePeer(deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
||||
cfg := wgtypes.PeerConfig{
|
||||
PublicKey: id.ToPublicKey(),
|
||||
Remove: true,
|
||||
}
|
||||
|
||||
err := r.wg.ConfigureDevice(string(deviceId), wgtypes.Config{ReplacePeers: false, Peers: []wgtypes.PeerConfig{cfg}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
121
internal/adapters/wireguard_integration_test.go
Normal file
121
internal/adapters/wireguard_integration_test.go
Normal file
@ -0,0 +1,121 @@
|
||||
//go:build integration
|
||||
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// setup WireGuard manager with no linked store
|
||||
func setup(t *testing.T) *wgRepo {
|
||||
if getProcessOwner() != "root" {
|
||||
t.Fatalf("this tests need to be executed as root user")
|
||||
}
|
||||
|
||||
repo := NewWireGuardRepository()
|
||||
|
||||
return repo
|
||||
}
|
||||
|
||||
func getProcessOwner() string {
|
||||
stdout, err := exec.Command("ps", "-o", "user=", "-p", strconv.Itoa(os.Getpid())).Output()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return strings.TrimSpace(string(stdout))
|
||||
}
|
||||
|
||||
func Test_wgRepository_GetInterfaces(t *testing.T) {
|
||||
mgr := setup(t)
|
||||
|
||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
||||
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
interfaceName2 := domain.InterfaceIdentifier("wg_test_002")
|
||||
defer mgr.DeleteInterface(context.Background(), interfaceName2)
|
||||
err = mgr.SaveInterface(context.Background(), interfaceName2, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
interfaces, err := mgr.GetInterfaces(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, interfaces, 2)
|
||||
for _, iface := range interfaces {
|
||||
assert.True(t, iface.Identifier == interfaceName || iface.Identifier == interfaceName2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireGuardCreateInterface(t *testing.T) {
|
||||
mgr := setup(t)
|
||||
|
||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||
ipAddress := "10.11.12.13"
|
||||
ipV6Address := "1337:d34d:b33f::2"
|
||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
||||
|
||||
err := mgr.SaveInterface(context.Background(), interfaceName, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||
pi.Addresses = []domain.Cidr{
|
||||
{Ip: domain.IpAddress(net.ParseIP(ipAddress)), NetLength: 24, Bits: 32},
|
||||
{Ip: domain.IpAddress(net.ParseIP(ipV6Address)), NetLength: 64, Bits: 128},
|
||||
}
|
||||
return pi, nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validate that the interface has been created
|
||||
cmd := exec.Command("ip", "addr")
|
||||
out, err := cmd.CombinedOutput()
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(out), interfaceName)
|
||||
assert.Contains(t, string(out), ipAddress)
|
||||
assert.Contains(t, string(out), ipV6Address)
|
||||
}
|
||||
|
||||
func TestWireGuardUpdateInterface(t *testing.T) {
|
||||
mgr := setup(t)
|
||||
|
||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
||||
|
||||
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := exec.Command("ip", "addr")
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(out), interfaceName)
|
||||
|
||||
ipAddress := "10.11.12.13"
|
||||
ipV6Address := "1337:d34d:b33f::2"
|
||||
err = mgr.SaveInterface(context.Background(), interfaceName, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||
pi.Addresses = []domain.Cidr{
|
||||
{Ip: domain.IpAddress(net.ParseIP(ipAddress)), NetLength: 24, Bits: 32},
|
||||
{Ip: domain.IpAddress(net.ParseIP(ipV6Address)), NetLength: 64, Bits: 128},
|
||||
}
|
||||
return pi, nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validate that the interface has been updated
|
||||
cmd = exec.Command("ip", "addr")
|
||||
out, err = cmd.CombinedOutput()
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, string(out), interfaceName)
|
||||
assert.Contains(t, string(out), ipAddress)
|
||||
assert.Contains(t, string(out), ipV6Address)
|
||||
}
|
367
internal/app/auth.go
Normal file
367
internal/app/auth.go
Normal file
@ -0,0 +1,367 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
evbus "github.com/vardius/message-bus"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type authUserManager interface {
|
||||
Get(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||
Register(ctx context.Context, user *domain.User) error
|
||||
}
|
||||
|
||||
type authenticator struct {
|
||||
cfg *config.Auth
|
||||
bus evbus.MessageBus
|
||||
|
||||
oauthAuthenticators map[string]domain.OauthAuthenticator
|
||||
ldapAuthenticators map[string]domain.LdapAuthenticator
|
||||
|
||||
users authUserManager
|
||||
}
|
||||
|
||||
func newAuthenticator(cfg *config.Auth, bus evbus.MessageBus, users authUserManager) (*authenticator, error) {
|
||||
a := &authenticator{
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
users: users,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := a.setupExternalAuthProviders(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *authenticator) setupExternalAuthProviders(ctx context.Context) error {
|
||||
extUrl, err := url.Parse(a.cfg.CallbackUrlPrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse external url: %w", err)
|
||||
}
|
||||
|
||||
a.oauthAuthenticators = make(map[string]domain.OauthAuthenticator, len(a.cfg.OpenIDConnect)+len(a.cfg.OAuth))
|
||||
a.ldapAuthenticators = make(map[string]domain.LdapAuthenticator, len(a.cfg.Ldap))
|
||||
|
||||
for i := range a.cfg.OpenIDConnect { // OIDC
|
||||
providerCfg := &a.cfg.OpenIDConnect[i]
|
||||
providerId := strings.ToLower(providerCfg.ProviderName)
|
||||
|
||||
if _, exists := a.oauthAuthenticators[providerId]; exists {
|
||||
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
|
||||
}
|
||||
|
||||
redirectUrl := *extUrl
|
||||
redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback")
|
||||
|
||||
provider, err := newOidcAuthenticator(ctx, redirectUrl.String(), providerCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup oidc authentication provider %s: %w", providerCfg.ProviderName, err)
|
||||
}
|
||||
a.oauthAuthenticators[providerId] = provider
|
||||
}
|
||||
for i := range a.cfg.OAuth { // PLAIN OAUTH
|
||||
providerCfg := &a.cfg.OAuth[i]
|
||||
providerId := strings.ToLower(providerCfg.ProviderName)
|
||||
|
||||
if _, exists := a.oauthAuthenticators[providerId]; exists {
|
||||
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
|
||||
}
|
||||
|
||||
redirectUrl := *extUrl
|
||||
redirectUrl.Path = path.Join(redirectUrl.Path, "/auth/login/", providerId, "/callback")
|
||||
|
||||
provider, err := newPlainOauthAuthenticator(ctx, redirectUrl.String(), providerCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup oauth authentication provider %s: %w", providerId, err)
|
||||
}
|
||||
a.oauthAuthenticators[providerId] = provider
|
||||
}
|
||||
for i := range a.cfg.Ldap { // LDAP
|
||||
providerCfg := &a.cfg.Ldap[i]
|
||||
providerId := strings.ToLower(providerCfg.URL)
|
||||
|
||||
if _, exists := a.ldapAuthenticators[providerId]; exists {
|
||||
return fmt.Errorf("auth provider with name %s is already registerd", providerId)
|
||||
}
|
||||
|
||||
provider, err := newLdapAuthenticator(ctx, providerCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup ldap authentication provider %s: %w", providerId, err)
|
||||
}
|
||||
a.ldapAuthenticators[providerId] = provider
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *authenticator) GetExternalLoginProviders(_ context.Context) []domain.LoginProviderInfo {
|
||||
authProviders := make([]domain.LoginProviderInfo, 0, len(a.cfg.OAuth)+len(a.cfg.OpenIDConnect))
|
||||
|
||||
for _, provider := range a.cfg.OpenIDConnect {
|
||||
providerId := strings.ToLower(provider.ProviderName)
|
||||
providerName := provider.DisplayName
|
||||
if providerName == "" {
|
||||
providerName = provider.ProviderName
|
||||
}
|
||||
authProviders = append(authProviders, domain.LoginProviderInfo{
|
||||
Identifier: providerId,
|
||||
Name: providerName,
|
||||
ProviderUrl: fmt.Sprintf("/auth/login/%s/init", providerId),
|
||||
CallbackUrl: fmt.Sprintf("/auth/login/%s/callback", providerId),
|
||||
})
|
||||
}
|
||||
|
||||
for _, provider := range a.cfg.OAuth {
|
||||
providerId := strings.ToLower(provider.ProviderName)
|
||||
providerName := provider.DisplayName
|
||||
if providerName == "" {
|
||||
providerName = provider.ProviderName
|
||||
}
|
||||
authProviders = append(authProviders, domain.LoginProviderInfo{
|
||||
Identifier: providerId,
|
||||
Name: providerName,
|
||||
ProviderUrl: fmt.Sprintf("%s/%s/init", a.cfg.CallbackUrlPrefix, providerId),
|
||||
CallbackUrl: fmt.Sprintf("%s/%s/callback", a.cfg.CallbackUrlPrefix, providerId),
|
||||
})
|
||||
}
|
||||
|
||||
return authProviders
|
||||
}
|
||||
|
||||
func (a *authenticator) IsUserValid(ctx context.Context, id domain.UserIdentifier) bool {
|
||||
user, err := a.users.Get(ctx, id)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if user.IsDisabled() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// region password authentication
|
||||
|
||||
func (a *authenticator) PlainLogin(ctx context.Context, username, password string) (*domain.User, error) {
|
||||
// Validate form input
|
||||
username = strings.TrimSpace(username)
|
||||
password = strings.TrimSpace(password)
|
||||
if username == "" || password == "" {
|
||||
return nil, fmt.Errorf("missing username or password")
|
||||
}
|
||||
|
||||
user, err := a.passwordAuthentication(ctx, domain.UserIdentifier(username), password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
a.bus.Publish(TopicAuthLogin, user.Identifier)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *authenticator) passwordAuthentication(ctx context.Context, identifier domain.UserIdentifier, password string) (*domain.User, error) {
|
||||
var ldapUserInfo *domain.AuthenticatorUserInfo
|
||||
var ldapProvider domain.LdapAuthenticator
|
||||
|
||||
var userInDatabase = false
|
||||
var userSource domain.UserSource
|
||||
existingUser, err := a.users.Get(ctx, identifier)
|
||||
if err == nil {
|
||||
userInDatabase = true
|
||||
userSource = domain.UserSourceDatabase
|
||||
} else {
|
||||
// search user in ldap if registration is enabled
|
||||
for _, ldapAuth := range a.ldapAuthenticators {
|
||||
if !ldapAuth.RegistrationEnabled() {
|
||||
continue
|
||||
}
|
||||
rawUserInfo, err := ldapAuth.GetUserInfo(context.Background(), identifier)
|
||||
if err != nil {
|
||||
continue // user not found / other ldap error
|
||||
}
|
||||
ldapUserInfo, err = ldapAuth.ParseUserInfo(rawUserInfo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// ldap user found
|
||||
userSource = domain.UserSourceLdap
|
||||
ldapProvider = ldapAuth
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if userSource == "" {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
switch userSource {
|
||||
case domain.UserSourceDatabase:
|
||||
err = existingUser.CheckPassword(password)
|
||||
case domain.UserSourceLdap:
|
||||
err = ldapProvider.PlaintextAuthentication(identifier, password)
|
||||
default:
|
||||
err = errors.New("no authentication backend available")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to authenticate: %w", err)
|
||||
}
|
||||
|
||||
if !userInDatabase {
|
||||
user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapProvider.GetName(), ldapProvider.RegistrationEnabled())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to process user information: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
} else {
|
||||
return existingUser, nil
|
||||
}
|
||||
}
|
||||
|
||||
// endregion password authentication
|
||||
|
||||
// region oauth authentication
|
||||
|
||||
func (a *authenticator) OauthLoginStep1(_ context.Context, providerId string) (authCodeUrl, state, nonce string, err error) {
|
||||
oauthProvider, ok := a.oauthAuthenticators[providerId]
|
||||
if !ok {
|
||||
return "", "", "", fmt.Errorf("missing oauth provider %s", providerId)
|
||||
}
|
||||
|
||||
// Prepare authentication flow, set state cookies
|
||||
state, err = a.randString(16)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("failed to generate state: %w", err)
|
||||
}
|
||||
|
||||
switch oauthProvider.GetType() {
|
||||
case domain.AuthenticatorTypeOAuth:
|
||||
authCodeUrl = oauthProvider.AuthCodeURL(state)
|
||||
case domain.AuthenticatorTypeOidc:
|
||||
nonce, err = a.randString(16)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
authCodeUrl = oauthProvider.AuthCodeURL(state, oidc.Nonce(nonce))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *authenticator) randString(nByte int) (string, error) {
|
||||
b := make([]byte, nByte)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func (a *authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
|
||||
oauthProvider, ok := a.oauthAuthenticators[providerId]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing oauth provider %s", providerId)
|
||||
}
|
||||
|
||||
oauth2Token, err := oauthProvider.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to exchange code: %w", err)
|
||||
}
|
||||
|
||||
rawUserInfo, err := oauthProvider.GetUserInfo(ctx, oauth2Token, nonce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to fetch user information: %w", err)
|
||||
}
|
||||
|
||||
userInfo, err := oauthProvider.ParseUserInfo(rawUserInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse user information: %w", err)
|
||||
}
|
||||
|
||||
user, err := a.processUserInfo(ctx, userInfo, domain.UserSourceOauth, oauthProvider.GetName(), oauthProvider.RegistrationEnabled())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to process user information: %w", err)
|
||||
}
|
||||
|
||||
a.bus.Publish(TopicAuthLogin, user.Identifier)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *authenticator) processUserInfo(ctx context.Context, userInfo *domain.AuthenticatorUserInfo, source domain.UserSource, provider string, withReg bool) (*domain.User, error) {
|
||||
// Search user in backend
|
||||
user, err := a.users.Get(ctx, userInfo.Identifier)
|
||||
switch {
|
||||
case err != nil && withReg:
|
||||
user, err = a.registerNewUser(ctx, userInfo, source, provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register user: %w", err)
|
||||
}
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("registration disabled, cannot create missing user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *authenticator) registerNewUser(ctx context.Context, userInfo *domain.AuthenticatorUserInfo, source domain.UserSource, provider string) (*domain.User, error) {
|
||||
// convert user info to domain.User
|
||||
user := &domain.User{
|
||||
Identifier: userInfo.Identifier,
|
||||
Email: userInfo.Email,
|
||||
Source: source,
|
||||
ProviderName: provider,
|
||||
IsAdmin: userInfo.IsAdmin,
|
||||
Firstname: userInfo.Firstname,
|
||||
Lastname: userInfo.Lastname,
|
||||
Phone: userInfo.Phone,
|
||||
Department: userInfo.Department,
|
||||
}
|
||||
|
||||
err := a.users.Register(ctx, user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register new user: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a *authenticator) getAuthenticatorConfig(id string) (interface{}, error) {
|
||||
for i := range a.cfg.OpenIDConnect {
|
||||
if a.cfg.OpenIDConnect[i].ProviderName == id {
|
||||
return a.cfg.OpenIDConnect[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
for i := range a.cfg.OAuth {
|
||||
if a.cfg.OAuth[i].ProviderName == id {
|
||||
return a.cfg.OAuth[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no configuration for authenticator id %s", id)
|
||||
}
|
||||
|
||||
// endregion oauth authentication
|
168
internal/app/auth_ldap.go
Normal file
168
internal/app/auth_ldap.go
Normal file
@ -0,0 +1,168 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type ldapAuthenticator struct {
|
||||
cfg *config.LdapProvider
|
||||
}
|
||||
|
||||
func newLdapAuthenticator(_ context.Context, cfg *config.LdapProvider) (*ldapAuthenticator, error) {
|
||||
var provider = &ldapAuthenticator{}
|
||||
|
||||
provider.cfg = cfg
|
||||
|
||||
dn, err := ldap.ParseDN(cfg.AdminGroupDN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse admin group DN: %w", err)
|
||||
}
|
||||
provider.cfg.FieldMap = provider.getLdapFieldMapping(cfg.FieldMap)
|
||||
provider.cfg.ParsedAdminGroupDN = dn
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (l ldapAuthenticator) GetName() string {
|
||||
return l.cfg.ProviderName
|
||||
}
|
||||
|
||||
func (l ldapAuthenticator) RegistrationEnabled() bool {
|
||||
return l.cfg.RegistrationEnabled
|
||||
}
|
||||
|
||||
func (l ldapAuthenticator) PlaintextAuthentication(userId domain.UserIdentifier, plainPassword string) error {
|
||||
conn, err := ldapConnect(l.cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup connection: %w", err)
|
||||
}
|
||||
defer ldapDisconnect(conn)
|
||||
|
||||
attrs := []string{"dn"}
|
||||
|
||||
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
l.cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
|
||||
loginFilter, attrs, nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to search in ldap: %w", err)
|
||||
}
|
||||
|
||||
if len(sr.Entries) == 0 {
|
||||
return domain.ErrNotFound
|
||||
}
|
||||
|
||||
if len(sr.Entries) > 1 {
|
||||
return domain.ErrNotUnique
|
||||
}
|
||||
|
||||
// Bind as the user to verify their password
|
||||
userDN := sr.Entries[0].DN
|
||||
err = conn.Bind(userDN, plainPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid credentials: %w", err)
|
||||
}
|
||||
_ = conn.Unbind()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l ldapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIdentifier) (map[string]interface{}, error) {
|
||||
conn, err := ldapConnect(l.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup connection: %w", err)
|
||||
}
|
||||
defer ldapDisconnect(conn)
|
||||
|
||||
attrs := ldapSearchAttributes(&l.cfg.FieldMap)
|
||||
|
||||
loginFilter := strings.Replace(l.cfg.LoginFilter, "{{login_identifier}}", string(userId), -1)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
l.cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 20, false, // 20 second time limit
|
||||
loginFilter, attrs, nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search in ldap: %w", err)
|
||||
}
|
||||
|
||||
if len(sr.Entries) == 0 {
|
||||
return nil, domain.ErrNotFound
|
||||
}
|
||||
|
||||
if len(sr.Entries) > 1 {
|
||||
return nil, domain.ErrNotUnique
|
||||
}
|
||||
|
||||
users := ldapConvertEntries(sr, &l.cfg.FieldMap)
|
||||
|
||||
return users[0], nil
|
||||
}
|
||||
|
||||
func (l ldapAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, err := ldapIsMemberOf(raw[l.cfg.FieldMap.GroupMembership].([][]byte), l.cfg.ParsedAdminGroupDN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check admin group: %w", err)
|
||||
}
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, l.cfg.FieldMap.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, l.cfg.FieldMap.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, l.cfg.FieldMap.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, l.cfg.FieldMap.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, l.cfg.FieldMap.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, l.cfg.FieldMap.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (l ldapAuthenticator) getLdapFieldMapping(f config.LdapFields) config.LdapFields {
|
||||
defaultMap := config.LdapFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "mail",
|
||||
Email: "mail",
|
||||
Firstname: "givenName",
|
||||
Lastname: "sn",
|
||||
Phone: "telephoneNumber",
|
||||
Department: "department",
|
||||
},
|
||||
GroupMembership: "memberOf",
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.GroupMembership != "" {
|
||||
defaultMap.GroupMembership = f.GroupMembership
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
}
|
149
internal/app/auth_oauth.go
Normal file
149
internal/app/auth_oauth.go
Normal file
@ -0,0 +1,149 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type plainOauthAuthenticator struct {
|
||||
name string
|
||||
cfg *oauth2.Config
|
||||
userInfoEndpoint string
|
||||
client *http.Client
|
||||
userInfoMapping config.OauthFields
|
||||
registrationEnabled bool
|
||||
}
|
||||
|
||||
func newPlainOauthAuthenticator(_ context.Context, callbackUrl string, cfg *config.OAuthProvider) (*plainOauthAuthenticator, error) {
|
||||
var provider = &plainOauthAuthenticator{}
|
||||
|
||||
provider.name = cfg.ProviderName
|
||||
provider.client = &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
provider.cfg = &oauth2.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: cfg.AuthURL,
|
||||
TokenURL: cfg.TokenURL,
|
||||
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||
},
|
||||
RedirectURL: callbackUrl,
|
||||
Scopes: cfg.Scopes,
|
||||
}
|
||||
provider.userInfoEndpoint = cfg.UserInfoURL
|
||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (p plainOauthAuthenticator) GetName() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p plainOauthAuthenticator) RegistrationEnabled() bool {
|
||||
return p.registrationEnabled
|
||||
}
|
||||
|
||||
func (p plainOauthAuthenticator) GetType() domain.AuthenticatorType {
|
||||
return domain.AuthenticatorTypeOAuth
|
||||
}
|
||||
|
||||
func (p plainOauthAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
return p.cfg.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (p plainOauthAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
return p.cfg.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (p plainOauthAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, _ string) (map[string]interface{}, error) {
|
||||
req, err := http.NewRequest("GET", p.userInfoEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user info get request: %w", err)
|
||||
}
|
||||
req.Header.Add("Authorization", "Bearer "+token.AccessToken)
|
||||
req.WithContext(ctx)
|
||||
|
||||
response, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
contents, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
var userFields map[string]interface{}
|
||||
err = json.Unmarshal(contents, &userFields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse user info: %w", err)
|
||||
}
|
||||
|
||||
return userFields, nil
|
||||
}
|
||||
|
||||
func (p plainOauthAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, p.userInfoMapping.IsAdmin, ""))
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, p.userInfoMapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, p.userInfoMapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, p.userInfoMapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, p.userInfoMapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, p.userInfoMapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, p.userInfoMapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func getOauthFieldMapping(f config.OauthFields) config.OauthFields {
|
||||
defaultMap := config.OauthFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "sub",
|
||||
Email: "email",
|
||||
Firstname: "given_name",
|
||||
Lastname: "family_name",
|
||||
Phone: "phone",
|
||||
Department: "department",
|
||||
},
|
||||
IsAdmin: "admin_flag",
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.IsAdmin != "" {
|
||||
defaultMap.IsAdmin = f.IsAdmin
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
}
|
107
internal/app/auth_oidc.go
Normal file
107
internal/app/auth_oidc.go
Normal file
@ -0,0 +1,107 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type oidcAuthenticator struct {
|
||||
name string
|
||||
provider *oidc.Provider
|
||||
verifier *oidc.IDTokenVerifier
|
||||
cfg *oauth2.Config
|
||||
userInfoMapping config.OauthFields
|
||||
registrationEnabled bool
|
||||
}
|
||||
|
||||
func newOidcAuthenticator(ctx context.Context, callbackUrl string, cfg *config.OpenIDConnectProvider) (*oidcAuthenticator, error) {
|
||||
var err error
|
||||
var provider = &oidcAuthenticator{}
|
||||
|
||||
provider.name = cfg.ProviderName
|
||||
provider.provider, err = oidc.NewProvider(context.Background(), cfg.BaseUrl) // use new context here, see https://github.com/coreos/go-oidc/issues/339
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new oidc provider: %w", err)
|
||||
}
|
||||
provider.verifier = provider.provider.Verifier(&oidc.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
})
|
||||
|
||||
scopes := []string{oidc.ScopeOpenID}
|
||||
scopes = append(scopes, cfg.ExtraScopes...)
|
||||
provider.cfg = &oauth2.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
Endpoint: provider.provider.Endpoint(),
|
||||
RedirectURL: callbackUrl,
|
||||
Scopes: scopes,
|
||||
}
|
||||
provider.userInfoMapping = getOauthFieldMapping(cfg.FieldMap)
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (o oidcAuthenticator) GetName() string {
|
||||
return o.name
|
||||
}
|
||||
|
||||
func (o oidcAuthenticator) RegistrationEnabled() bool {
|
||||
return o.registrationEnabled
|
||||
}
|
||||
|
||||
func (o oidcAuthenticator) GetType() domain.AuthenticatorType {
|
||||
return domain.AuthenticatorTypeOidc
|
||||
}
|
||||
|
||||
func (o oidcAuthenticator) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
return o.cfg.AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
func (o oidcAuthenticator) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
return o.cfg.Exchange(ctx, code, opts...)
|
||||
}
|
||||
|
||||
func (o oidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]interface{}, error) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("token does not contain id_token")
|
||||
}
|
||||
idToken, err := o.verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to validate id_token: %w", err)
|
||||
}
|
||||
if idToken.Nonce != nonce {
|
||||
return nil, errors.New("nonce mismatch")
|
||||
}
|
||||
|
||||
var tokenFields map[string]interface{}
|
||||
if err = idToken.Claims(&tokenFields); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
||||
}
|
||||
|
||||
return tokenFields, nil
|
||||
}
|
||||
|
||||
func (o oidcAuthenticator) ParseUserInfo(raw map[string]interface{}) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin, _ := strconv.ParseBool(internal.MapDefaultString(raw, o.userInfoMapping.IsAdmin, ""))
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, o.userInfoMapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, o.userInfoMapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, o.userInfoMapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, o.userInfoMapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, o.userInfoMapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, o.userInfoMapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
114
internal/app/core.go
Normal file
114
internal/app/core.go
Normal file
@ -0,0 +1,114 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
evbus "github.com/vardius/message-bus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// region global-repositories
|
||||
|
||||
type wireGuardRepo interface {
|
||||
GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error)
|
||||
GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error)
|
||||
GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error)
|
||||
GetPeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) (*domain.PhysicalPeer, error)
|
||||
SaveInterface(_ context.Context, id domain.InterfaceIdentifier, updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error)) error
|
||||
DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error
|
||||
SavePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier, updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error)) error
|
||||
DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error
|
||||
}
|
||||
|
||||
type dbRepo interface {
|
||||
GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error)
|
||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
|
||||
FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error)
|
||||
SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.Interface) (*domain.Interface, error)) error
|
||||
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
||||
GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error)
|
||||
GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error)
|
||||
FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) ([]domain.Peer, error)
|
||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||
FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error)
|
||||
SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error
|
||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||
FindUsers(ctx context.Context, search string) ([]domain.User, error)
|
||||
SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error
|
||||
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||
}
|
||||
|
||||
// endregion global-repositories
|
||||
|
||||
type App struct {
|
||||
Config *config.Config
|
||||
bus evbus.MessageBus
|
||||
db dbRepo
|
||||
wg wireGuardRepo
|
||||
|
||||
Authenticator *authenticator
|
||||
Users *userManager
|
||||
WireGuard *wireGuardManager
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, bus evbus.MessageBus, db dbRepo, wg wireGuardRepo) (*App, error) {
|
||||
users, err := newUserManager(cfg, bus, db, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth, err := newAuthenticator(&cfg.Auth, bus, users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wireGuard, err := newWireGuardManager(cfg, bus, wg, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &App{
|
||||
Config: cfg,
|
||||
bus: bus,
|
||||
db: db,
|
||||
wg: wg,
|
||||
|
||||
Authenticator: auth,
|
||||
Users: users,
|
||||
WireGuard: wireGuard,
|
||||
}
|
||||
|
||||
if a.Config.Core.ImportExisting {
|
||||
err := a.WireGuard.ImportNewInterfaces(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if a.Config.Core.RestoreState {
|
||||
err := a.WireGuard.RestoreInterfaceState(context.Background(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func HandleProgramArgs(cfg *config.Config, db *gorm.DB, wg wireGuardRepo) (exit bool, err error) {
|
||||
migrationSource := flag.String("migrateFrom", "", "path to v1 database file or DSN")
|
||||
migrationDbType := flag.String("migrateFromType", string(config.DatabaseSQLite), "old database type, either mysql, mssql, postgres or sqlite")
|
||||
flag.Parse()
|
||||
|
||||
if *migrationSource != "" {
|
||||
err = migrateFromV1(cfg, db, wg, *migrationSource, *migrationDbType)
|
||||
exit = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
6
internal/app/eventbus.go
Normal file
6
internal/app/eventbus.go
Normal file
@ -0,0 +1,6 @@
|
||||
package app
|
||||
|
||||
const TopicUserCreated = "user:created"
|
||||
const TopicUserRegistered = "user:registered"
|
||||
const TopicUserDisabled = "user:disabled"
|
||||
const TopicAuthLogin = "auth:login"
|
137
internal/app/ldap_utils.go
Normal file
137
internal/app/ldap_utils.go
Normal file
@ -0,0 +1,137 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
)
|
||||
|
||||
type rawLdapUser map[string]interface{}
|
||||
|
||||
func ldapFindAllUsers(conn *ldap.Conn, baseDn, filter string, fields *config.LdapFields) ([]rawLdapUser, error) {
|
||||
// Search all users
|
||||
attrs := ldapSearchAttributes(fields)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
baseDn,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter, attrs, nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search: %w", err)
|
||||
}
|
||||
|
||||
results := ldapConvertEntries(sr, fields)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func ldapConnect(cfg *config.LdapProvider) (*ldap.Conn, error) {
|
||||
tlsConfig := &tls.Config{InsecureSkipVerify: !cfg.CertValidation}
|
||||
if cfg.TlsCertificatePath != "" {
|
||||
certificate, err := os.ReadFile(cfg.TlsCertificatePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load TLS certificate: %w", err)
|
||||
|
||||
}
|
||||
|
||||
key, err := os.ReadFile(cfg.TlsKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load TLS key: %w", err)
|
||||
}
|
||||
|
||||
keyPair, err := tls.X509KeyPair(certificate, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate X509 keypair: %w", err)
|
||||
|
||||
}
|
||||
tlsConfig = &tls.Config{Certificates: []tls.Certificate{keyPair}}
|
||||
}
|
||||
|
||||
conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(tlsConfig))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial error: %w", err)
|
||||
}
|
||||
|
||||
if cfg.StartTLS { // Reconnect with TLS
|
||||
if err = conn.StartTLS(tlsConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to start TLS on connection: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = conn.Bind(cfg.BindUser, cfg.BindPass); err != nil {
|
||||
return nil, fmt.Errorf("failed to bind to LDAP: %w", err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func ldapDisconnect(conn *ldap.Conn) {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func ldapConvertEntries(sr *ldap.SearchResult, fields *config.LdapFields) []rawLdapUser {
|
||||
users := make([]rawLdapUser, len(sr.Entries))
|
||||
|
||||
for i, entry := range sr.Entries {
|
||||
userData := make(rawLdapUser)
|
||||
userData[fields.UserIdentifier] = entry.DN
|
||||
userData[fields.Email] = entry.GetAttributeValue(fields.Email)
|
||||
userData[fields.Firstname] = entry.GetAttributeValue(fields.Firstname)
|
||||
userData[fields.Lastname] = entry.GetAttributeValue(fields.Lastname)
|
||||
userData[fields.Phone] = entry.GetAttributeValue(fields.Phone)
|
||||
userData[fields.Department] = entry.GetAttributeValue(fields.Department)
|
||||
userData[fields.GroupMembership] = entry.GetRawAttributeValues(fields.GroupMembership)
|
||||
|
||||
users[i] = userData
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func ldapSearchAttributes(fields *config.LdapFields) []string {
|
||||
attrs := []string{"dn", fields.UserIdentifier}
|
||||
|
||||
if fields.Email != "" {
|
||||
attrs = append(attrs, fields.Email)
|
||||
}
|
||||
if fields.Firstname != "" {
|
||||
attrs = append(attrs, fields.Firstname)
|
||||
}
|
||||
if fields.Lastname != "" {
|
||||
attrs = append(attrs, fields.Lastname)
|
||||
}
|
||||
if fields.Phone != "" {
|
||||
attrs = append(attrs, fields.Phone)
|
||||
}
|
||||
if fields.Department != "" {
|
||||
attrs = append(attrs, fields.Department)
|
||||
}
|
||||
if fields.GroupMembership != "" {
|
||||
attrs = append(attrs, fields.GroupMembership)
|
||||
}
|
||||
|
||||
return internal.UniqueStringSlice(attrs)
|
||||
}
|
||||
|
||||
// ldapIsMemberOf checks if the groupData array contains the group DN
|
||||
func ldapIsMemberOf(groupData [][]byte, groupDN *ldap.DN) (bool, error) {
|
||||
for _, group := range groupData {
|
||||
dn, err := ldap.ParseDN(string(group))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to parse group DN: %w", err)
|
||||
}
|
||||
if groupDN.Equal(dn) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
384
internal/app/migrate_v1.go
Normal file
384
internal/app/migrate_v1.go
Normal file
@ -0,0 +1,384 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/h44z/wg-portal/internal/adapters"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func migrateFromV1(cfg *config.Config, db *gorm.DB, wg wireGuardRepo, source, typ string) error {
|
||||
sourceType := config.SupportedDatabase(typ)
|
||||
switch sourceType {
|
||||
case config.DatabaseMySQL, config.DatabasePostgres, config.DatabaseMsSQL:
|
||||
case config.DatabaseSQLite:
|
||||
if _, err := os.Stat(source); os.IsNotExist(err) {
|
||||
return fmt.Errorf("invalid source database: %w", err)
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported database")
|
||||
}
|
||||
|
||||
oldDb, err := adapters.NewDatabase(config.DatabaseConfig{
|
||||
Type: sourceType,
|
||||
DSN: source,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open old database: %w", err)
|
||||
}
|
||||
|
||||
// check if old db is a valid WireGuard Portal v1 database
|
||||
type DatabaseMigrationInfo struct {
|
||||
Version string `gorm:"primaryKey"`
|
||||
Applied time.Time
|
||||
}
|
||||
|
||||
lastVersion := DatabaseMigrationInfo{}
|
||||
err = oldDb.Order("applied desc, version desc").FirstOrInit(&lastVersion).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to validate old database: %w", err)
|
||||
}
|
||||
latestVersion := "1.0.9"
|
||||
if lastVersion.Version != latestVersion {
|
||||
return fmt.Errorf("unsupported old version, update to database version %s first: %w", latestVersion, err)
|
||||
}
|
||||
|
||||
logrus.Infof("Found valid V1 database with version: %s", lastVersion.Version)
|
||||
|
||||
if err := migrateV1Users(oldDb, db); err != nil {
|
||||
return fmt.Errorf("user migration failed: %w", err)
|
||||
}
|
||||
|
||||
if err := migrateV1Interfaces(oldDb, db); err != nil {
|
||||
return fmt.Errorf("user migration failed: %w", err)
|
||||
}
|
||||
|
||||
if err := migrateV1Peers(oldDb, db); err != nil {
|
||||
return fmt.Errorf("peer migration failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
||||
type User struct {
|
||||
Email string `gorm:"primaryKey"`
|
||||
Source string
|
||||
IsAdmin bool
|
||||
Firstname string
|
||||
Lastname string
|
||||
Phone string
|
||||
Password string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
var oldUsers []User
|
||||
err := oldDb.Find(&oldUsers).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch old user records: %w", err)
|
||||
}
|
||||
|
||||
for _, oldUser := range oldUsers {
|
||||
var deletionTime *time.Time
|
||||
deletionReason := ""
|
||||
if oldUser.DeletedAt.Valid {
|
||||
delTime := oldUser.DeletedAt.Time
|
||||
deletionTime = &delTime
|
||||
deletionReason = "disabled prior to migration"
|
||||
}
|
||||
newUser := domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "v1migrator",
|
||||
UpdatedBy: "v1migrator",
|
||||
CreatedAt: oldUser.CreatedAt,
|
||||
UpdatedAt: oldUser.UpdatedAt,
|
||||
},
|
||||
Identifier: domain.UserIdentifier(oldUser.Email),
|
||||
Email: oldUser.Email,
|
||||
Source: domain.UserSource(oldUser.Source),
|
||||
ProviderName: "",
|
||||
IsAdmin: oldUser.IsAdmin,
|
||||
Firstname: oldUser.Firstname,
|
||||
Lastname: oldUser.Lastname,
|
||||
Phone: oldUser.Phone,
|
||||
Department: "",
|
||||
Notes: "",
|
||||
Password: domain.PrivateString(oldUser.Password),
|
||||
Disabled: deletionTime,
|
||||
DisabledReason: deletionReason,
|
||||
Locked: nil,
|
||||
LockedReason: "",
|
||||
LinkedPeerCount: 0,
|
||||
}
|
||||
|
||||
if err := newDb.Save(&newUser).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
|
||||
type Device struct {
|
||||
Type string
|
||||
DeviceName string `gorm:"primaryKey"`
|
||||
DisplayName string
|
||||
PrivateKey string
|
||||
ListenPort int
|
||||
FirewallMark int32
|
||||
PublicKey string
|
||||
Mtu int
|
||||
IPsStr string
|
||||
DNSStr string
|
||||
RoutingTable string
|
||||
PreUp string
|
||||
PostUp string
|
||||
PreDown string
|
||||
PostDown string
|
||||
SaveConfig bool
|
||||
DefaultEndpoint string
|
||||
DefaultAllowedIPsStr string
|
||||
DefaultPersistentKeepalive int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
var oldDevices []Device
|
||||
err := oldDb.Find(&oldDevices).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch old device records: %w", err)
|
||||
}
|
||||
|
||||
for _, oldDevice := range oldDevices {
|
||||
ips, err := domain.CidrsFromString(oldDevice.IPsStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse %s ip addresses: %w", oldDevice.DeviceName, err)
|
||||
}
|
||||
networks := make([]domain.Cidr, len(ips))
|
||||
for i, ip := range ips {
|
||||
networks[i] = domain.CidrFromIpNet(*ip.IpNet())
|
||||
}
|
||||
newInterface := domain.Interface{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "v1migrator",
|
||||
UpdatedBy: "v1migrator",
|
||||
CreatedAt: oldDevice.CreatedAt,
|
||||
UpdatedAt: oldDevice.UpdatedAt,
|
||||
},
|
||||
Identifier: domain.InterfaceIdentifier(oldDevice.DeviceName),
|
||||
KeyPair: domain.KeyPair{
|
||||
PrivateKey: oldDevice.PrivateKey,
|
||||
PublicKey: oldDevice.PublicKey,
|
||||
},
|
||||
ListenPort: oldDevice.ListenPort,
|
||||
Addresses: ips,
|
||||
DnsStr: "",
|
||||
DnsSearchStr: "",
|
||||
Mtu: oldDevice.Mtu,
|
||||
FirewallMark: oldDevice.FirewallMark,
|
||||
RoutingTable: oldDevice.RoutingTable,
|
||||
PreUp: oldDevice.PreUp,
|
||||
PostUp: oldDevice.PostUp,
|
||||
PreDown: oldDevice.PreDown,
|
||||
PostDown: oldDevice.PostDown,
|
||||
SaveConfig: oldDevice.SaveConfig,
|
||||
DisplayName: oldDevice.DisplayName,
|
||||
Type: domain.InterfaceType(oldDevice.Type),
|
||||
DriverType: "",
|
||||
Disabled: nil,
|
||||
DisabledReason: "",
|
||||
PeerDefNetworkStr: domain.CidrsToString(networks),
|
||||
PeerDefDnsStr: oldDevice.DNSStr,
|
||||
PeerDefDnsSearchStr: "",
|
||||
PeerDefEndpoint: oldDevice.DefaultEndpoint,
|
||||
PeerDefAllowedIPsStr: oldDevice.DefaultAllowedIPsStr,
|
||||
PeerDefMtu: oldDevice.Mtu,
|
||||
PeerDefPersistentKeepalive: oldDevice.DefaultPersistentKeepalive,
|
||||
PeerDefFirewallMark: 0,
|
||||
PeerDefRoutingTable: "",
|
||||
PeerDefPreUp: "",
|
||||
PeerDefPostUp: "",
|
||||
PeerDefPreDown: "",
|
||||
PeerDefPostDown: "",
|
||||
}
|
||||
|
||||
if err := newDb.Save(&newInterface).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
||||
type Peer struct {
|
||||
UID string
|
||||
DeviceName string `gorm:"index"`
|
||||
Identifier string
|
||||
Email string `gorm:"index" form:"mail" binding:"required,email"`
|
||||
IgnoreGlobalSettings bool
|
||||
PublicKey string `gorm:"primaryKey"`
|
||||
PresharedKey string
|
||||
AllowedIPsStr string
|
||||
AllowedIPsSrvStr string
|
||||
Endpoint string
|
||||
PersistentKeepalive int
|
||||
PrivateKey string
|
||||
IPsStr string
|
||||
DNSStr string
|
||||
Mtu int
|
||||
DeactivatedAt *time.Time `json:",omitempty"`
|
||||
DeactivatedReason string `json:",omitempty"`
|
||||
ExpiresAt *time.Time
|
||||
CreatedBy string
|
||||
UpdatedBy string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
var oldPeers []Peer
|
||||
err := oldDb.Find(&oldPeers).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch old peer records: %w", err)
|
||||
}
|
||||
|
||||
for _, oldPeer := range oldPeers {
|
||||
ips, err := domain.CidrsFromString(oldPeer.IPsStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse %s ip addresses: %w", oldPeer.PublicKey, err)
|
||||
}
|
||||
var disableTime *time.Time
|
||||
disableReason := ""
|
||||
if oldPeer.DeactivatedAt != nil {
|
||||
disTime := *oldPeer.DeactivatedAt
|
||||
disableTime = &disTime
|
||||
disableReason = oldPeer.DeactivatedReason
|
||||
}
|
||||
var expiryTime *time.Time
|
||||
if oldPeer.ExpiresAt != nil {
|
||||
expTime := *oldPeer.ExpiresAt
|
||||
expiryTime = &expTime
|
||||
}
|
||||
var iface domain.Interface
|
||||
var ifaceType domain.InterfaceType
|
||||
err = newDb.First(&iface, "identifier = ?", oldPeer.DeviceName).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find interface %s for peer %s: %w", oldPeer.DeviceName, oldPeer.PublicKey, err)
|
||||
}
|
||||
switch iface.Type {
|
||||
case domain.InterfaceTypeClient:
|
||||
ifaceType = domain.InterfaceTypeServer
|
||||
case domain.InterfaceTypeServer:
|
||||
ifaceType = domain.InterfaceTypeClient
|
||||
case domain.InterfaceTypeAny:
|
||||
ifaceType = domain.InterfaceTypeAny
|
||||
}
|
||||
var user domain.User
|
||||
err = newDb.First(&user, "identifier = ?", oldPeer.Email).Error // migrated users use the email address as identifier
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("failed to find user %s for peer %s: %w", oldPeer.Email, oldPeer.PublicKey, err)
|
||||
}
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// create dummy user
|
||||
now := time.Now()
|
||||
user = domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "v1migrator",
|
||||
UpdatedBy: "v1migrator",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
Identifier: domain.UserIdentifier(oldPeer.Email),
|
||||
Email: oldPeer.Email,
|
||||
Source: domain.UserSourceDatabase,
|
||||
ProviderName: "",
|
||||
IsAdmin: false,
|
||||
Locked: &now,
|
||||
LockedReason: "migration dummy user",
|
||||
Notes: "created by migration from v1",
|
||||
}
|
||||
|
||||
if err := newDb.Save(&user).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate dummy user %s: %w", oldPeer.Email, err)
|
||||
}
|
||||
}
|
||||
newPeer := domain.Peer{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "v1migrator",
|
||||
UpdatedBy: "v1migrator",
|
||||
CreatedAt: oldPeer.CreatedAt,
|
||||
UpdatedAt: oldPeer.UpdatedAt,
|
||||
},
|
||||
Endpoint: domain.StringConfigOption{
|
||||
Value: oldPeer.Endpoint, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
EndpointPublicKey: iface.PublicKey,
|
||||
AllowedIPsStr: domain.StringConfigOption{
|
||||
Value: oldPeer.AllowedIPsStr, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
ExtraAllowedIPsStr: oldPeer.AllowedIPsSrvStr,
|
||||
PresharedKey: domain.PreSharedKey(oldPeer.PresharedKey),
|
||||
PersistentKeepalive: domain.IntConfigOption{
|
||||
Value: oldPeer.PersistentKeepalive, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
DisplayName: oldPeer.Identifier,
|
||||
Identifier: domain.PeerIdentifier(oldPeer.PublicKey),
|
||||
UserIdentifier: user.Identifier,
|
||||
InterfaceIdentifier: iface.Identifier,
|
||||
Temporary: nil,
|
||||
Disabled: disableTime,
|
||||
DisabledReason: disableReason,
|
||||
ExpiresAt: expiryTime,
|
||||
Notes: "",
|
||||
Interface: domain.PeerInterfaceConfig{
|
||||
KeyPair: domain.KeyPair{
|
||||
PrivateKey: oldPeer.PrivateKey,
|
||||
PublicKey: oldPeer.PublicKey,
|
||||
},
|
||||
Type: ifaceType,
|
||||
Addresses: ips,
|
||||
DnsStr: domain.StringConfigOption{
|
||||
Value: oldPeer.DNSStr, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
DnsSearchStr: domain.StringConfigOption{
|
||||
Value: iface.PeerDefDnsSearchStr, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
Mtu: domain.IntConfigOption{
|
||||
Value: oldPeer.Mtu, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
FirewallMark: domain.Int32ConfigOption{
|
||||
Value: iface.PeerDefFirewallMark, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
RoutingTable: domain.StringConfigOption{
|
||||
Value: iface.PeerDefRoutingTable, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
PreUp: domain.StringConfigOption{
|
||||
Value: iface.PeerDefPreUp, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
PostUp: domain.StringConfigOption{
|
||||
Value: iface.PeerDefPostUp, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
PreDown: domain.StringConfigOption{
|
||||
Value: iface.PeerDefPreDown, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
PostDown: domain.StringConfigOption{
|
||||
Value: iface.PeerDefPostDown, Overridable: !oldPeer.IgnoreGlobalSettings,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := newDb.Save(&newPeer).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
128
internal/app/template.go
Normal file
128
internal/app/template.go
Normal file
@ -0,0 +1,128 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
htmlTemplate "html/template"
|
||||
"io"
|
||||
"text/template"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
//go:embed tpl_files/*
|
||||
var TemplateFiles embed.FS
|
||||
|
||||
type templateHandler struct {
|
||||
wireGuardTemplates *template.Template
|
||||
|
||||
mailHtmlTemplates *htmlTemplate.Template
|
||||
mailTextTemplates *template.Template
|
||||
}
|
||||
|
||||
func newTemplateHandler() (*templateHandler, error) {
|
||||
templateCache, err := template.New("WireGuard").ParseFS(TemplateFiles, "tpl_files/*.tpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mailHtmlTemplateCache, err := htmlTemplate.New("WireGuard").ParseFS(TemplateFiles, "tpl_files/*.gohtml")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse html template files: %w", err)
|
||||
}
|
||||
|
||||
mailTxtTemplateCache, err := template.New("WireGuard").ParseFS(TemplateFiles, "tpl_files/*.gotpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse text template files: %w", err)
|
||||
}
|
||||
|
||||
handler := &templateHandler{
|
||||
wireGuardTemplates: templateCache,
|
||||
mailHtmlTemplates: mailHtmlTemplateCache,
|
||||
mailTextTemplates: mailTxtTemplateCache,
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
func (c templateHandler) GetInterfaceConfig(cfg *domain.Interface, peers []*domain.Peer) (io.Reader, error) {
|
||||
var tplBuff bytes.Buffer
|
||||
|
||||
err := c.wireGuardTemplates.ExecuteTemplate(&tplBuff, "wg_interface.tpl", map[string]interface{}{
|
||||
"Interface": cfg,
|
||||
"Peers": peers,
|
||||
"Portal": map[string]interface{}{
|
||||
"Version": "unknown",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute interface template for %s: %w", cfg.Identifier, err)
|
||||
}
|
||||
|
||||
return &tplBuff, nil
|
||||
}
|
||||
|
||||
func (c templateHandler) GetPeerConfig(peer *domain.Peer) (io.Reader, error) {
|
||||
var tplBuff bytes.Buffer
|
||||
|
||||
err := c.wireGuardTemplates.ExecuteTemplate(&tplBuff, "wg_peer.tpl", map[string]interface{}{
|
||||
"Peer": peer,
|
||||
"Portal": map[string]interface{}{
|
||||
"Version": "unknown",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute peer template for %s: %w", peer.Identifier, err)
|
||||
}
|
||||
|
||||
return &tplBuff, nil
|
||||
}
|
||||
|
||||
func (c templateHandler) GetConfigMail(user *domain.User, peer *domain.Peer, link string) (io.Reader, io.Reader, error) {
|
||||
var tplBuff bytes.Buffer
|
||||
var htmlTplBuff bytes.Buffer
|
||||
|
||||
err := c.mailTextTemplates.ExecuteTemplate(&tplBuff, "mail_with_link.gotpl", map[string]interface{}{
|
||||
"User": user,
|
||||
"Peer": peer,
|
||||
"Link": link,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_link.gotpl")
|
||||
}
|
||||
|
||||
err = c.mailHtmlTemplates.ExecuteTemplate(&tplBuff, "mail_with_link.gohtml", map[string]interface{}{
|
||||
"User": user,
|
||||
"Peer": peer,
|
||||
"Link": link,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_link.gohtml")
|
||||
}
|
||||
|
||||
return &tplBuff, &htmlTplBuff, nil
|
||||
}
|
||||
|
||||
func (c templateHandler) GetConfigMailWithAttachment(user *domain.User, peer *domain.Peer) (io.Reader, io.Reader, error) {
|
||||
var tplBuff bytes.Buffer
|
||||
var htmlTplBuff bytes.Buffer
|
||||
|
||||
err := c.mailTextTemplates.ExecuteTemplate(&tplBuff, "mail_with_attachment.gotpl", map[string]interface{}{
|
||||
"User": user,
|
||||
"Peer": peer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gotpl")
|
||||
}
|
||||
|
||||
err = c.mailHtmlTemplates.ExecuteTemplate(&tplBuff, "mail_with_attachment.gohtml", map[string]interface{}{
|
||||
"User": user,
|
||||
"Peer": peer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_attachment.gohtml")
|
||||
}
|
||||
|
||||
return &tplBuff, &htmlTplBuff, nil
|
||||
}
|
0
internal/app/tpl_files/mail_with_attachment.gohtml
Normal file
0
internal/app/tpl_files/mail_with_attachment.gohtml
Normal file
1
internal/app/tpl_files/mail_with_attachment.gotpl
Normal file
1
internal/app/tpl_files/mail_with_attachment.gotpl
Normal file
@ -0,0 +1 @@
|
||||
package tpl_files
|
0
internal/app/tpl_files/mail_with_link.gohtml
Normal file
0
internal/app/tpl_files/mail_with_link.gohtml
Normal file
0
internal/app/tpl_files/mail_with_link.gotpl
Normal file
0
internal/app/tpl_files/mail_with_link.gotpl
Normal file
@ -1,14 +1,18 @@
|
||||
# AUTOGENERATED FILE - DO NOT EDIT
|
||||
# -WGP- Interface: {{ .Interface.DeviceName }} / Updated: {{ .Interface.UpdatedAt }} / Created: {{ .Interface.CreatedAt }}
|
||||
# -WGP- Interface display name: {{ .Interface.DisplayName }}
|
||||
# -WGP- Interface mode: {{ .Interface.Type }}
|
||||
# -WGP- PublicKey = {{ .Interface.PublicKey }}
|
||||
# This file uses wg-quick format. See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
|
||||
|
||||
# -WGP- WIREGUARD PORTAL CONFIGURATION FILE, version {{ .Portal.Version }}
|
||||
# Lines starting with the -WGP- tag are used by the WireGuard Portal configuration parser.
|
||||
|
||||
[Interface]
|
||||
# -WGP- Interface: {{ .Interface.Identifier }} | Updated: {{ .Interface.UpdatedAt }} | Created: {{ .Interface.CreatedAt }}
|
||||
# -WGP- Display name: {{ .Interface.DisplayName }}
|
||||
# -WGP- Interface mode: {{ .Interface.Type }}
|
||||
# -WGP- PublicKey = {{ .Interface.KeyPair.PublicKey }}
|
||||
|
||||
# Core settings
|
||||
PrivateKey = {{ .Interface.PrivateKey }}
|
||||
Address = {{ .Interface.IPsStr }}
|
||||
PrivateKey = {{ .Interface.KeyPair.PrivateKey }}
|
||||
Address = {{ .Interface.AddressStr }}
|
||||
|
||||
# Misc. settings (optional)
|
||||
{{- if ne .Interface.ListenPort 0}}
|
||||
@ -17,8 +21,8 @@ ListenPort = {{ .Interface.ListenPort }}
|
||||
{{- if ne .Interface.Mtu 0}}
|
||||
MTU = {{.Interface.Mtu}}
|
||||
{{- end}}
|
||||
{{- if and (ne .Interface.DNSStr "") (eq $.Interface.Type "client")}}
|
||||
DNS = {{ .Interface.DNSStr }}
|
||||
{{- if and (ne .Interface.DnsStr "") (eq $.Interface.Type "client")}}
|
||||
DNS = {{ .Interface.DnsStr }}
|
||||
{{- end}}
|
||||
{{- if ne .Interface.FirewallMark 0}}
|
||||
FwMark = {{.Interface.FirewallMark}}
|
||||
@ -49,22 +53,19 @@ PostDown = {{ .Interface.PostDown }}
|
||||
#
|
||||
|
||||
{{range .Peers}}
|
||||
{{- if not .DeactivatedAt}}
|
||||
# -WGP- Peer: {{.Identifier}} / Updated: {{.UpdatedAt}} / Created: {{.CreatedAt}}
|
||||
# -WGP- Peer email: {{.Email}}
|
||||
{{- if .PrivateKey}}
|
||||
# -WGP- PrivateKey: {{.PrivateKey}}
|
||||
{{- end}}
|
||||
{{- if not .DisabledAt}}
|
||||
[Peer]
|
||||
{{- if $.FriendlyNames}}
|
||||
# friendly_name = {{ .Identifier }}
|
||||
# -WGP- Peer: {{.Uid}} | Updated: {{.UpdatedAt}} | Created: {{.CreatedAt}}
|
||||
# -WGP- Display name: {{ .Identifier }}
|
||||
{{- if .KeyPair.PrivateKey}}
|
||||
# -WGP- PrivateKey: {{.KeyPair.PrivateKey}}
|
||||
{{- end}}
|
||||
PublicKey = {{ .PublicKey }}
|
||||
PublicKey = {{ .KeyPair.PublicKey }}
|
||||
{{- if .PresharedKey}}
|
||||
PresharedKey = {{ .PresharedKey }}
|
||||
{{- end}}
|
||||
{{- if eq $.Interface.Type "server"}}
|
||||
AllowedIPs = {{ .IPsStr }}{{if ne .AllowedIPsSrvStr ""}}, {{ .AllowedIPsSrvStr }}{{end}}
|
||||
AllowedIPs = {{ .AddressStr }}{{if ne .ExtraAllowedIPsStr ""}}, {{ .ExtraAllowedIPsStr }}{{end}}
|
||||
{{- end}}
|
||||
{{- if eq $.Interface.Type "client"}}
|
||||
{{- if .AllowedIPsStr}}
|
||||
@ -78,4 +79,4 @@ Endpoint = {{ .Endpoint }}
|
||||
PersistentKeepalive = {{ .PersistentKeepalive }}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{end}}
|
||||
{{end}}
|
60
internal/app/tpl_files/wg_peer.tpl
Normal file
60
internal/app/tpl_files/wg_peer.tpl
Normal file
@ -0,0 +1,60 @@
|
||||
# AUTOGENERATED FILE - DO NOT EDIT
|
||||
# This file uses wg-quick format. See https://man7.org/linux/man-pages/man8/wg-quick.8.html#CONFIGURATION
|
||||
|
||||
# -WGP- WIREGUARD PORTAL CONFIGURATION FILE, version {{ .Portal.Version }}
|
||||
# Lines starting with the -WGP- tag are used by the WireGuard Portal configuration parser.
|
||||
|
||||
[Interface]
|
||||
# -WGP- Peer: {{.Peer.Identifier}} | Updated: {{.Peer.UpdatedAt}} | Created: {{.Peer.CreatedAt}}
|
||||
# -WGP- Display name: {{ .Peer.DisplayName }}
|
||||
# -WGP- PublicKey: {{ .Peer.KeyPair.PublicKey }}
|
||||
{{- if eq .Peer.Interface.Type "server"}}
|
||||
# -WGP- Peer type: client
|
||||
{{else}}
|
||||
# -WGP- Peer type: server
|
||||
{{- end}}
|
||||
|
||||
# Core settings
|
||||
PrivateKey = {{ .Peer.KeyPair.PrivateKey }}
|
||||
Address = {{ .Peer.Interface.AddressStr }}
|
||||
|
||||
# Misc. settings (optional)
|
||||
{{- if .Peer.Interface.DnsStr.GetValue}}
|
||||
DNS = {{ .Peer.Interface.DnsStr.GetValue }}
|
||||
{{- end}}
|
||||
{{- if ne .Peer.Interface.Mtu.GetValue 0}}
|
||||
MTU = {{ .Peer.Interface.Mtu.GetValue }}
|
||||
{{- end}}
|
||||
{{- if ne .Peer.Interface.FirewallMark.GetValue 0}}
|
||||
FwMark = {{ .Peer.Interface.FirewallMark.GetValue }}
|
||||
{{- end}}
|
||||
{{- if ne .Peer.Interface.RoutingTable.GetValue ""}}
|
||||
Table = {{ .Peer.Interface.RoutingTable.GetValue }}
|
||||
{{- end}}
|
||||
|
||||
# Interface hooks (optional)
|
||||
{{- if .Peer.Interface.PreUp.GetValue}}
|
||||
PreUp = {{ .Peer.Interface.PreUp.GetValue }}
|
||||
{{- end}}
|
||||
{{- if .Peer.Interface.PostUp.GetValue}}
|
||||
PostUp = {{ .Peer.Interface.PostUp.GetValue }}
|
||||
{{- end}}
|
||||
{{- if .Peer.Interface.PreDown.GetValue}}
|
||||
PreDown = {{ .Peer.Interface.PreDown.GetValue }}
|
||||
{{- end}}
|
||||
{{- if .Peer.Interface.PostDown.GetValue}}
|
||||
PostDown = {{ .Peer.Interface.PostDown.GetValue }}
|
||||
{{- end}}
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{ .Peer.Interface.PublicKey }}
|
||||
Endpoint = {{ .Peer.Endpoint.GetValue }}
|
||||
{{- if .Peer.AllowedIPsStr.GetValue}}
|
||||
AllowedIPs = {{ .Peer.AllowedIPsStr.GetValue }}
|
||||
{{- end}}
|
||||
{{- if .Peer.PresharedKey}}
|
||||
PresharedKey = {{ .Peer.PresharedKey }}
|
||||
{{- end}}
|
||||
{{- if ne .Peer.PersistentKeepalive.GetValue 0}}
|
||||
PersistentKeepalive = {{ .Peer.PersistentKeepalive.GetValue }}
|
||||
{{- end}}
|
448
internal/app/user.go
Normal file
448
internal/app/user.go
Normal file
@ -0,0 +1,448 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
evbus "github.com/vardius/message-bus"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// region local-dependencies
|
||||
|
||||
type userRepo interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
GetAllUsers(ctx context.Context) ([]domain.User, error)
|
||||
FindUsers(ctx context.Context, search string) ([]domain.User, error)
|
||||
SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error
|
||||
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
|
||||
}
|
||||
|
||||
type peerRepo interface {
|
||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||
}
|
||||
|
||||
// endregion local-dependencies
|
||||
|
||||
type userManager struct {
|
||||
cfg *config.Config
|
||||
bus evbus.MessageBus
|
||||
|
||||
syncInterval time.Duration
|
||||
users userRepo
|
||||
peers peerRepo
|
||||
}
|
||||
|
||||
func newUserManager(cfg *config.Config, bus evbus.MessageBus, users userRepo, peers peerRepo) (*userManager, error) {
|
||||
m := &userManager{
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
|
||||
syncInterval: 10 * time.Second,
|
||||
users: users,
|
||||
peers: peers,
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m userManager) Register(ctx context.Context, user *domain.User) error {
|
||||
err := m.New(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.bus.Publish(TopicUserRegistered, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m userManager) New(ctx context.Context, user *domain.User) error {
|
||||
if user.Identifier == "" {
|
||||
return errors.New("missing user identifier")
|
||||
}
|
||||
|
||||
err := m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
u.Identifier = user.Identifier
|
||||
u.Email = user.Email
|
||||
u.Source = user.Source
|
||||
u.IsAdmin = user.IsAdmin
|
||||
u.Firstname = user.Firstname
|
||||
u.Lastname = user.Lastname
|
||||
u.Phone = user.Phone
|
||||
u.Department = user.Department
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save user: %w", err)
|
||||
}
|
||||
|
||||
m.bus.Publish(TopicUserCreated, user)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m userManager) StartBackgroundJobs(ctx context.Context) {
|
||||
go m.runLdapSynchronizationService(ctx)
|
||||
}
|
||||
|
||||
func (m userManager) Get(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||
user, err := m.users.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load peer %s: %w", id, err)
|
||||
}
|
||||
peers, _ := m.peers.GetUserPeers(ctx, id) // ignore error, list will be empty in error case
|
||||
|
||||
user.LinkedPeerCount = len(peers)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m userManager) GetAll(ctx context.Context) ([]domain.User, error) {
|
||||
users, err := m.users.GetAllUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load users: %w", err)
|
||||
}
|
||||
|
||||
ch := make(chan *domain.User)
|
||||
wg := sync.WaitGroup{}
|
||||
workers := int(math.Min(float64(len(users)), 10))
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for user := range ch {
|
||||
peers, _ := m.peers.GetUserPeers(ctx, user.Identifier) // ignore error, list will be empty in error case
|
||||
user.LinkedPeerCount = len(peers)
|
||||
}
|
||||
}()
|
||||
}
|
||||
for i := range users {
|
||||
ch <- &users[i]
|
||||
}
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m userManager) Update(ctx context.Context, user *domain.User) (*domain.User, error) {
|
||||
existingUser, err := m.users.GetUser(ctx, user.Identifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
if err := m.validateModifications(ctx, existingUser, user); err != nil {
|
||||
return nil, fmt.Errorf("update not allowed: %w", err)
|
||||
}
|
||||
|
||||
user.CopyCalculatedAttributes(existingUser)
|
||||
err = user.HashPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Password == "" { // keep old password
|
||||
user.Password = existingUser.Password
|
||||
}
|
||||
|
||||
err = m.users.SaveUser(ctx, existingUser.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
user.CopyCalculatedAttributes(u)
|
||||
return user, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update failure: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m userManager) Create(ctx context.Context, user *domain.User) (*domain.User, error) {
|
||||
existingUser, err := m.users.GetUser(ctx, user.Identifier)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err)
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, fmt.Errorf("user %s already exists", user.Identifier)
|
||||
}
|
||||
|
||||
if err := m.validateCreation(ctx, user); err != nil {
|
||||
return nil, fmt.Errorf("creation not allowed: %w", err)
|
||||
}
|
||||
|
||||
err = user.HashPassword()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
user.CopyCalculatedAttributes(u)
|
||||
return user, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creation failure: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m userManager) validateModifications(ctx context.Context, old, new *domain.User) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if err := old.EditAllowed(); err != nil {
|
||||
return fmt.Errorf("no access: %w", err)
|
||||
}
|
||||
|
||||
if err := old.CanChangePassword(); err != nil && string(new.Password) != "" {
|
||||
return fmt.Errorf("no access: %w", err)
|
||||
}
|
||||
|
||||
if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
|
||||
return fmt.Errorf("cannot remove own admin rights")
|
||||
}
|
||||
|
||||
if currentUser.Id == old.Identifier && new.IsDisabled() {
|
||||
return fmt.Errorf("cannot disable own user")
|
||||
}
|
||||
|
||||
if old.Source != new.Source {
|
||||
return fmt.Errorf("cannot change user source")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m userManager) validateCreation(ctx context.Context, new *domain.User) error {
|
||||
if new.Identifier == "" {
|
||||
return fmt.Errorf("invalid user identifier")
|
||||
}
|
||||
|
||||
if new.Source != domain.UserSourceDatabase {
|
||||
return fmt.Errorf("invalid user source: %s", new.Source)
|
||||
}
|
||||
|
||||
if string(new.Password) == "" {
|
||||
return fmt.Errorf("invalid password")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m userManager) runLdapSynchronizationService(ctx context.Context) {
|
||||
running := true
|
||||
for running {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
running = false
|
||||
continue
|
||||
case <-time.After(m.syncInterval):
|
||||
// select blocks until one of the cases evaluate to true
|
||||
}
|
||||
|
||||
for _, ldapCfg := range m.cfg.Auth.Ldap { // LDAP Auth providers
|
||||
if !ldapCfg.Synchronize {
|
||||
continue // sync disabled
|
||||
}
|
||||
|
||||
err := m.synchronizeLdapUsers(ctx, &ldapCfg)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to synchronize LDAP users for %s: %v", ldapCfg.ProviderName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m userManager) synchronizeLdapUsers(ctx context.Context, provider *config.LdapProvider) error {
|
||||
logrus.Tracef("starting to synchronize users for %s", provider.ProviderName)
|
||||
|
||||
dn, err := ldap.ParseDN(provider.AdminGroupDN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse admin group DN: %w", err)
|
||||
}
|
||||
provider.ParsedAdminGroupDN = dn
|
||||
|
||||
conn, err := ldapConnect(provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to setup LDAP connection: %w", err)
|
||||
}
|
||||
defer ldapDisconnect(conn)
|
||||
|
||||
rawUsers, err := ldapFindAllUsers(conn, provider.BaseDN, provider.SyncFilter, &provider.FieldMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Tracef("fetched %d raw ldap users...", len(rawUsers))
|
||||
|
||||
// Update existing LDAP users
|
||||
err = m.updateLdapUsers(ctx, provider.ProviderName, rawUsers, &provider.FieldMap, provider.ParsedAdminGroupDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Disable missing LDAP users
|
||||
if provider.DisableMissing {
|
||||
err = m.disableMissingLdapUsers(ctx, provider.ProviderName, rawUsers, &provider.FieldMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m userManager) updateLdapUsers(ctx context.Context, providerName string, rawUsers []rawLdapUser, fields *config.LdapFields, adminGroupDN *ldap.DN) error {
|
||||
for _, rawUser := range rawUsers {
|
||||
user, err := convertRawLdapUser(providerName, rawUser, fields, adminGroupDN)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("failed to convert LDAP data for %v: %w", rawUser["dn"], err)
|
||||
}
|
||||
|
||||
existingUser, err := m.users.GetUser(ctx, user.Identifier)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("find error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
if existingUser == nil {
|
||||
err := m.New(ctx, user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
}
|
||||
|
||||
if existingUser != nil && existingUser.Source == domain.UserSourceLdap && userChangedInLdap(existingUser, user) {
|
||||
err := m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
u.UpdatedAt = time.Now()
|
||||
u.UpdatedBy = "ldap_sync"
|
||||
u.Email = user.Email
|
||||
u.Firstname = user.Firstname
|
||||
u.Lastname = user.Lastname
|
||||
u.Phone = user.Phone
|
||||
u.Department = user.Department
|
||||
u.IsAdmin = user.IsAdmin
|
||||
u.Disabled = user.Disabled
|
||||
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m userManager) disableMissingLdapUsers(ctx context.Context, providerName string, rawUsers []rawLdapUser, fields *config.LdapFields) error {
|
||||
allUsers, err := m.users.GetAllUsers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, user := range allUsers {
|
||||
if user.Source != domain.UserSourceLdap {
|
||||
continue // ignore non ldap users
|
||||
}
|
||||
if user.ProviderName != providerName {
|
||||
continue // user was synchronized through different provider
|
||||
}
|
||||
if user.IsDisabled() {
|
||||
continue // ignore deactivated
|
||||
}
|
||||
|
||||
existsInLDAP := false
|
||||
for _, rawUser := range rawUsers {
|
||||
userId := domain.UserIdentifier(internal.MapDefaultString(rawUser, fields.UserIdentifier, ""))
|
||||
if user.Identifier == userId {
|
||||
existsInLDAP = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existsInLDAP {
|
||||
continue
|
||||
}
|
||||
|
||||
err := m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
|
||||
now := time.Now()
|
||||
u.Disabled = &now
|
||||
u.DisabledReason = "missing in ldap"
|
||||
return u, nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("disable error for user id %s: %w", user.Identifier, err)
|
||||
}
|
||||
|
||||
m.bus.Publish(TopicUserDisabled, user)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertRawLdapUser(providerName string, raw rawLdapUser, fields *config.LdapFields, adminGroupDN *ldap.DN) (*domain.User, error) {
|
||||
now := time.Now()
|
||||
|
||||
isAdmin, err := ldapIsMemberOf(raw[fields.GroupMembership].([][]byte), adminGroupDN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check admin group: %w", err)
|
||||
}
|
||||
|
||||
return &domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: "ldap_sync",
|
||||
UpdatedBy: "ldap_sync",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, fields.UserIdentifier, "")),
|
||||
Email: strings.ToLower(internal.MapDefaultString(raw, fields.Email, "")),
|
||||
Source: domain.UserSourceLdap,
|
||||
ProviderName: providerName,
|
||||
IsAdmin: isAdmin,
|
||||
Firstname: internal.MapDefaultString(raw, fields.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, fields.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, fields.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, fields.Department, ""),
|
||||
Notes: "",
|
||||
Password: "",
|
||||
Disabled: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func userChangedInLdap(dbUser, ldapUser *domain.User) bool {
|
||||
if dbUser.Firstname != ldapUser.Firstname {
|
||||
return true
|
||||
}
|
||||
if dbUser.Lastname != ldapUser.Lastname {
|
||||
return true
|
||||
}
|
||||
if dbUser.Email != ldapUser.Email {
|
||||
return true
|
||||
}
|
||||
if dbUser.Phone != ldapUser.Phone {
|
||||
return true
|
||||
}
|
||||
if dbUser.Department != ldapUser.Department {
|
||||
return true
|
||||
}
|
||||
|
||||
if dbUser.IsDisabled() != ldapUser.IsDisabled() {
|
||||
return true
|
||||
}
|
||||
|
||||
if dbUser.IsAdmin != ldapUser.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
523
internal/app/wireguard.go
Normal file
523
internal/app/wireguard.go
Normal file
@ -0,0 +1,523 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
evbus "github.com/vardius/message-bus"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// region local-dependencies
|
||||
|
||||
type wireGuardDatabaseRepo interface {
|
||||
GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error)
|
||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
|
||||
FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error)
|
||||
GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error)
|
||||
SaveInterface(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.Interface) (*domain.Interface, error)) error
|
||||
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
|
||||
GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error)
|
||||
FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) ([]domain.Peer, error)
|
||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||
FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error)
|
||||
SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error
|
||||
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
|
||||
}
|
||||
|
||||
// endregion local-dependencies
|
||||
|
||||
type wireGuardManager struct {
|
||||
cfg *config.Config
|
||||
bus evbus.MessageBus
|
||||
|
||||
db wireGuardDatabaseRepo
|
||||
wg wireGuardRepo
|
||||
}
|
||||
|
||||
func newWireGuardManager(cfg *config.Config, bus evbus.MessageBus, wgRepo wireGuardRepo, db wireGuardDatabaseRepo) (*wireGuardManager, error) {
|
||||
m := &wireGuardManager{
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
wg: wgRepo,
|
||||
db: db,
|
||||
}
|
||||
|
||||
m.connectToMessageBus()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) connectToMessageBus() {
|
||||
_ = m.bus.Subscribe(TopicUserCreated, m.handleUserCreationEvent)
|
||||
}
|
||||
|
||||
func (m wireGuardManager) handleUserCreationEvent(user *domain.User) {
|
||||
logrus.Errorf("Handling new user event for %s", user.Identifier)
|
||||
|
||||
err := m.CreateDefaultPeer(context.Background(), user)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to create default peer")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (m wireGuardManager) GetImportableInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
|
||||
physicalInterfaces, err := m.wg.GetInterfaces(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return physicalInterfaces, nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) ImportNewInterfaces(ctx context.Context, filter ...domain.InterfaceIdentifier) error {
|
||||
physicalInterfaces, err := m.wg.GetInterfaces(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if no filter is given, exclude already existing interfaces
|
||||
var excludedInterfaces []domain.InterfaceIdentifier
|
||||
if len(filter) == 0 {
|
||||
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, existingInterface := range existingInterfaces {
|
||||
excludedInterfaces = append(excludedInterfaces, existingInterface.Identifier)
|
||||
}
|
||||
}
|
||||
|
||||
for _, physicalInterface := range physicalInterfaces {
|
||||
if internal.SliceContains(excludedInterfaces, physicalInterface.Identifier) {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(filter) != 0 && !internal.SliceContains(filter, physicalInterface.Identifier) {
|
||||
continue
|
||||
}
|
||||
|
||||
logrus.Infof("importing new interface %s...", physicalInterface.Identifier)
|
||||
|
||||
physicalPeers, err := m.wg.GetPeers(ctx, physicalInterface.Identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.importInterface(ctx, &physicalInterface, physicalPeers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("import of %s failed: %w", physicalInterface.Identifier, err)
|
||||
}
|
||||
|
||||
logrus.Infof("imported new interface %s and %d peers", physicalInterface.Identifier, len(physicalPeers))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) importInterface(ctx context.Context, in *domain.PhysicalInterface, peers []domain.PhysicalPeer) error {
|
||||
now := time.Now()
|
||||
iface := domain.ConvertPhysicalInterface(in)
|
||||
iface.BaseModel = domain.BaseModel{
|
||||
CreatedBy: "importer",
|
||||
UpdatedBy: "importer",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
iface.PeerDefAllowedIPsStr = iface.AddressStr()
|
||||
|
||||
existingInterface, err := m.db.GetInterface(ctx, iface.Identifier)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
if existingInterface != nil {
|
||||
return errors.New("interface already exists")
|
||||
}
|
||||
|
||||
err = m.db.SaveInterface(ctx, iface.Identifier, func(_ *domain.Interface) (*domain.Interface, error) {
|
||||
return iface, nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("database save failed: %w", err)
|
||||
}
|
||||
|
||||
// import peers
|
||||
for _, peer := range peers {
|
||||
err = m.importPeer(ctx, iface, &peer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("import of peer %s failed: %w", peer.Identifier, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) importPeer(ctx context.Context, in *domain.Interface, p *domain.PhysicalPeer) error {
|
||||
now := time.Now()
|
||||
peer := domain.ConvertPhysicalPeer(p)
|
||||
peer.BaseModel = domain.BaseModel{
|
||||
CreatedBy: "importer",
|
||||
UpdatedBy: "importer",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
peer.InterfaceIdentifier = in.Identifier
|
||||
peer.EndpointPublicKey = in.PublicKey
|
||||
peer.AllowedIPsStr = domain.StringConfigOption{Value: in.PeerDefAllowedIPsStr, Overridable: true}
|
||||
peer.Interface.Addresses = p.AllowedIPs // use allowed IP's as the peer IP's
|
||||
peer.Interface.DnsStr = domain.StringConfigOption{Value: in.PeerDefDnsStr, Overridable: true}
|
||||
peer.Interface.DnsSearchStr = domain.StringConfigOption{Value: in.PeerDefDnsSearchStr, Overridable: true}
|
||||
peer.Interface.Mtu = domain.IntConfigOption{Value: in.PeerDefMtu, Overridable: true}
|
||||
peer.Interface.FirewallMark = domain.Int32ConfigOption{Value: in.PeerDefFirewallMark, Overridable: true}
|
||||
peer.Interface.RoutingTable = domain.StringConfigOption{Value: in.PeerDefRoutingTable, Overridable: true}
|
||||
peer.Interface.PreUp = domain.StringConfigOption{Value: in.PeerDefPreUp, Overridable: true}
|
||||
peer.Interface.PostUp = domain.StringConfigOption{Value: in.PeerDefPostUp, Overridable: true}
|
||||
peer.Interface.PreDown = domain.StringConfigOption{Value: in.PeerDefPreDown, Overridable: true}
|
||||
peer.Interface.PostDown = domain.StringConfigOption{Value: in.PeerDefPostDown, Overridable: true}
|
||||
|
||||
switch in.Type {
|
||||
case domain.InterfaceTypeAny:
|
||||
peer.Interface.Type = domain.InterfaceTypeAny
|
||||
peer.DisplayName = "Autodetected Peer (" + peer.Interface.PublicKey[0:8] + ")"
|
||||
case domain.InterfaceTypeClient:
|
||||
peer.Interface.Type = domain.InterfaceTypeServer
|
||||
peer.DisplayName = "Autodetected Endpoint (" + peer.Interface.PublicKey[0:8] + ")"
|
||||
case domain.InterfaceTypeServer:
|
||||
peer.Interface.Type = domain.InterfaceTypeClient
|
||||
peer.DisplayName = "Autodetected Client (" + peer.Interface.PublicKey[0:8] + ")"
|
||||
}
|
||||
|
||||
err := m.db.SavePeer(ctx, peer.Identifier, func(_ *domain.Peer) (*domain.Peer, error) {
|
||||
return peer, nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("database save failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) RestoreInterfaceState(ctx context.Context, updateDbOnError bool, filter ...domain.InterfaceIdentifier) error {
|
||||
interfaces, err := m.db.GetAllInterfaces(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if len(filter) != 0 && !internal.SliceContains(filter, iface.Identifier) {
|
||||
continue
|
||||
}
|
||||
|
||||
peers, err := m.db.GetInterfacePeers(ctx, iface.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load peers for %s: %w", iface.Identifier, err)
|
||||
}
|
||||
|
||||
physicalInterface, err := m.wg.GetInterface(ctx, iface.Identifier)
|
||||
if err != nil {
|
||||
// try to create a new interface
|
||||
err := m.wg.SaveInterface(ctx, iface.Identifier, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||
domain.MergeToPhysicalInterface(pi, &iface)
|
||||
|
||||
return pi, nil
|
||||
})
|
||||
if err != nil {
|
||||
if updateDbOnError {
|
||||
// disable interface in database as no physical interface exists
|
||||
_ = m.db.SaveInterface(ctx, iface.Identifier, func(in *domain.Interface) (*domain.Interface, error) {
|
||||
now := time.Now()
|
||||
in.Disabled = &now // set
|
||||
in.DisabledReason = "no physical interface available"
|
||||
return in, nil
|
||||
})
|
||||
}
|
||||
return fmt.Errorf("failed to create physical interface %s: %w", iface.Identifier, err)
|
||||
}
|
||||
|
||||
// restore peers
|
||||
for _, peer := range peers {
|
||||
err := m.wg.SavePeer(ctx, iface.Identifier, peer.Identifier, func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
|
||||
domain.MergeToPhysicalPeer(pp, &peer)
|
||||
return pp, nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create physical peer %s: %w", peer.Identifier, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if physicalInterface.DeviceUp != !iface.IsDisabled() {
|
||||
// try to move interface to stored state
|
||||
err := m.wg.SaveInterface(ctx, iface.Identifier, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||
pi.DeviceUp = !iface.IsDisabled()
|
||||
|
||||
return pi, nil
|
||||
})
|
||||
if err != nil {
|
||||
if updateDbOnError {
|
||||
// disable interface in database as no physical interface is available
|
||||
_ = m.db.SaveInterface(ctx, iface.Identifier, func(in *domain.Interface) (*domain.Interface, error) {
|
||||
if iface.IsDisabled() {
|
||||
now := time.Now()
|
||||
in.Disabled = &now // set
|
||||
in.DisabledReason = "no physical interface active"
|
||||
} else {
|
||||
in.Disabled = nil
|
||||
in.DisabledReason = ""
|
||||
}
|
||||
return in, nil
|
||||
})
|
||||
}
|
||||
return fmt.Errorf("failed to change physical interface state for %s: %w", iface.Identifier, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) CreateDefaultPeer(ctx context.Context, user *domain.User) error {
|
||||
// TODO: implement
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) {
|
||||
return m.db.GetInterfaceAndPeers(ctx, id)
|
||||
}
|
||||
|
||||
func (m wireGuardManager) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
||||
return m.db.GetAllInterfaces(ctx)
|
||||
}
|
||||
|
||||
func (m wireGuardManager) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||
return m.db.GetUserPeers(ctx, id)
|
||||
}
|
||||
|
||||
func (m wireGuardManager) PrepareInterface(ctx context.Context) (*domain.Interface, error) {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
kp, err := domain.NewFreshKeypair()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate keys: %w", err)
|
||||
}
|
||||
|
||||
id, err := m.getNewInterfaceName(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate new identifier: %w", err)
|
||||
}
|
||||
|
||||
ipv4, ipv6, err := m.getFreshIpConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate new ip config: %w", err)
|
||||
}
|
||||
|
||||
freshInterface := &domain.Interface{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: string(currentUser.Id),
|
||||
UpdatedBy: string(currentUser.Id),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Identifier: id,
|
||||
KeyPair: kp,
|
||||
ListenPort: 0, // TODO
|
||||
Addresses: []domain.Cidr{ipv4, ipv6},
|
||||
DnsStr: "",
|
||||
DnsSearchStr: "",
|
||||
Mtu: 1420,
|
||||
FirewallMark: 0,
|
||||
RoutingTable: "",
|
||||
PreUp: "",
|
||||
PostUp: "",
|
||||
PreDown: "",
|
||||
PostDown: "",
|
||||
SaveConfig: false,
|
||||
DisplayName: string(id),
|
||||
Type: domain.InterfaceTypeServer,
|
||||
DriverType: "",
|
||||
Disabled: nil,
|
||||
DisabledReason: "",
|
||||
PeerDefNetworkStr: "", // TODO
|
||||
PeerDefDnsStr: "", // TODO
|
||||
PeerDefDnsSearchStr: "",
|
||||
PeerDefEndpoint: "",
|
||||
PeerDefAllowedIPsStr: "",
|
||||
PeerDefMtu: 1420,
|
||||
PeerDefPersistentKeepalive: 16,
|
||||
PeerDefFirewallMark: 0,
|
||||
PeerDefRoutingTable: "",
|
||||
PeerDefPreUp: "",
|
||||
PeerDefPostUp: "",
|
||||
PeerDefPreDown: "",
|
||||
PeerDefPostDown: "",
|
||||
}
|
||||
|
||||
return freshInterface, nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) getNewInterfaceName(ctx context.Context) (domain.InterfaceIdentifier, error) {
|
||||
namePrefix := "wg"
|
||||
nameSuffix := 1
|
||||
|
||||
existingInterfaces, err := m.db.GetAllInterfaces(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var name domain.InterfaceIdentifier
|
||||
for {
|
||||
name = domain.InterfaceIdentifier(fmt.Sprintf("%s%d", namePrefix, nameSuffix))
|
||||
|
||||
conflict := false
|
||||
for _, in := range existingInterfaces {
|
||||
if in.Identifier == name {
|
||||
conflict = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !conflict {
|
||||
break
|
||||
}
|
||||
|
||||
nameSuffix++
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) getFreshIpConfig(ctx context.Context) (ipV4, ipV6 domain.Cidr, err error) {
|
||||
ips, err := m.db.GetInterfaceIps(ctx)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to get existing IP addresses: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
ipV4, _ = domain.CidrFromString("10.6.6.1/24")
|
||||
ipV6, _ = domain.CidrFromString("fdfd:d3ad:c0de:1234::1/64")
|
||||
|
||||
for {
|
||||
ipV4Conflict := false
|
||||
ipV6Conflict := false
|
||||
for _, usedIps := range ips {
|
||||
for _, ip := range usedIps {
|
||||
if ipV4 == ip {
|
||||
ipV4Conflict = true
|
||||
}
|
||||
|
||||
if ipV6 == ip {
|
||||
ipV6Conflict = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ipV4Conflict && !ipV6Conflict {
|
||||
break
|
||||
}
|
||||
|
||||
if ipV4Conflict {
|
||||
ipV4 = ipV4.NextSubnet()
|
||||
}
|
||||
|
||||
if ipV6Conflict {
|
||||
ipV6 = ipV6.NextSubnet()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m wireGuardManager) CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error) {
|
||||
existingInterface, err := m.db.GetInterface(ctx, in.Identifier)
|
||||
if err != nil && !errors.Is(err, domain.ErrNotFound) {
|
||||
return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
|
||||
}
|
||||
if existingInterface != nil {
|
||||
return nil, fmt.Errorf("interface %s already exists", in.Identifier)
|
||||
}
|
||||
|
||||
if err := m.validateCreation(ctx, existingInterface, in); err != nil {
|
||||
return nil, fmt.Errorf("creation not allowed: %w", err)
|
||||
}
|
||||
|
||||
err = m.db.SaveInterface(ctx, in.Identifier, func(i *domain.Interface) (*domain.Interface, error) {
|
||||
in.CopyCalculatedAttributes(i)
|
||||
|
||||
err = m.wg.SaveInterface(ctx, in.Identifier, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||
domain.MergeToPhysicalInterface(pi, in)
|
||||
return pi, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create physical interface %s: %w", in.Identifier, err)
|
||||
}
|
||||
|
||||
return in, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creation failure: %w", err)
|
||||
}
|
||||
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error) {
|
||||
existingInterface, err := m.db.GetInterface(ctx, in.Identifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
|
||||
}
|
||||
|
||||
if err := m.validateModifications(ctx, existingInterface, in); err != nil {
|
||||
return nil, fmt.Errorf("update not allowed: %w", err)
|
||||
}
|
||||
|
||||
err = m.db.SaveInterface(ctx, in.Identifier, func(i *domain.Interface) (*domain.Interface, error) {
|
||||
in.CopyCalculatedAttributes(i)
|
||||
|
||||
err = m.wg.SaveInterface(ctx, in.Identifier, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||
domain.MergeToPhysicalInterface(pi, in)
|
||||
return pi, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update physical interface %s: %w", in.Identifier, err)
|
||||
}
|
||||
|
||||
return in, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("update failure: %w", err)
|
||||
}
|
||||
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) validateModifications(ctx context.Context, old, new *domain.Interface) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m wireGuardManager) validateCreation(ctx context.Context, old, new *domain.Interface) error {
|
||||
currentUser := domain.GetUserInfo(ctx)
|
||||
|
||||
if new.Identifier == "" {
|
||||
return fmt.Errorf("invalid interface identifier")
|
||||
}
|
||||
|
||||
if !currentUser.IsAdmin {
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthContext contains all information that the AuthProvider needs to perform the authentication.
|
||||
type AuthContext struct {
|
||||
Username string // email or username
|
||||
Password string
|
||||
Callback string // callback for OIDC
|
||||
}
|
||||
|
||||
type AuthProviderType string
|
||||
|
||||
const (
|
||||
AuthProviderTypePassword AuthProviderType = "password"
|
||||
AuthProviderTypeOauth AuthProviderType = "oauth"
|
||||
)
|
||||
|
||||
// AuthProvider is a interface that can be implemented by different authentication providers like LDAP, OAUTH, ...
|
||||
type AuthProvider interface {
|
||||
GetName() string
|
||||
GetType() AuthProviderType
|
||||
GetPriority() int // lower number = higher priority
|
||||
|
||||
Login(*AuthContext) (string, error)
|
||||
Logout(*AuthContext) error
|
||||
GetUserModel(*AuthContext) (*User, error)
|
||||
|
||||
SetupRoutes(routes *gin.RouterGroup)
|
||||
}
|
@ -1,210 +0,0 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/h44z/wg-portal/internal/authentication"
|
||||
ldapconfig "github.com/h44z/wg-portal/internal/ldap"
|
||||
"github.com/h44z/wg-portal/internal/users"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Provider implements a password login method for an LDAP backend.
|
||||
type Provider struct {
|
||||
config *ldapconfig.Config
|
||||
}
|
||||
|
||||
func New(cfg *ldapconfig.Config) (*Provider, error) {
|
||||
p := &Provider{
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
// test ldap connectivity
|
||||
client, err := p.open()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to open ldap connection")
|
||||
}
|
||||
defer p.close(client)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetName return provider name
|
||||
func (Provider) GetName() string {
|
||||
return string(users.UserSourceLdap)
|
||||
}
|
||||
|
||||
// GetType return provider type
|
||||
func (Provider) GetType() authentication.AuthProviderType {
|
||||
return authentication.AuthProviderTypePassword
|
||||
}
|
||||
|
||||
// GetPriority return provider priority
|
||||
func (Provider) GetPriority() int {
|
||||
return 1 // LDAP password provider
|
||||
}
|
||||
|
||||
func (provider Provider) SetupRoutes(_ *gin.RouterGroup) {
|
||||
// nothing here
|
||||
}
|
||||
|
||||
func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) {
|
||||
username := strings.ToLower(ctx.Username)
|
||||
password := ctx.Password
|
||||
|
||||
// Validate input
|
||||
if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" {
|
||||
return "", errors.New("empty username or password")
|
||||
}
|
||||
|
||||
client, err := provider.open()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to open ldap connection")
|
||||
}
|
||||
defer provider.close(client)
|
||||
|
||||
// Search for the given username
|
||||
attrs := []string{"dn", provider.config.EmailAttribute}
|
||||
loginFilter := strings.Replace(provider.config.LoginFilter, "{{login_identifier}}", username, -1)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
provider.config.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
loginFilter,
|
||||
attrs,
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := client.Search(searchRequest)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to find user in ldap")
|
||||
}
|
||||
|
||||
if len(sr.Entries) != 1 {
|
||||
return "", errors.Errorf("invalid amount of ldap entries (%d)", len(sr.Entries))
|
||||
}
|
||||
|
||||
// Bind as the user to verify their password
|
||||
userDN := sr.Entries[0].DN
|
||||
err = client.Bind(userDN, password)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "invalid credentials")
|
||||
}
|
||||
|
||||
return sr.Entries[0].GetAttributeValue(provider.config.EmailAttribute), nil
|
||||
}
|
||||
|
||||
func (provider Provider) Logout(_ *authentication.AuthContext) error {
|
||||
return nil // nothing here
|
||||
}
|
||||
|
||||
func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) {
|
||||
username := strings.ToLower(ctx.Username)
|
||||
|
||||
// Validate input
|
||||
if strings.Trim(username, " ") == "" {
|
||||
return nil, errors.New("empty username")
|
||||
}
|
||||
|
||||
client, err := provider.open()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to open ldap connection")
|
||||
}
|
||||
defer provider.close(client)
|
||||
|
||||
// Search for the given username
|
||||
attrs := []string{"dn", provider.config.EmailAttribute, provider.config.FirstNameAttribute, provider.config.LastNameAttribute,
|
||||
provider.config.PhoneAttribute, provider.config.GroupMemberAttribute}
|
||||
loginFilter := strings.Replace(provider.config.LoginFilter, "{{login_identifier}}", username, -1)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
provider.config.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
loginFilter,
|
||||
attrs,
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := client.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to find user in ldap")
|
||||
}
|
||||
|
||||
if len(sr.Entries) != 1 {
|
||||
return nil, errors.Wrapf(err, "invalid amount of ldap entries (%d)", len(sr.Entries))
|
||||
}
|
||||
|
||||
user := &authentication.User{
|
||||
Firstname: sr.Entries[0].GetAttributeValue(provider.config.FirstNameAttribute),
|
||||
Lastname: sr.Entries[0].GetAttributeValue(provider.config.LastNameAttribute),
|
||||
Email: sr.Entries[0].GetAttributeValue(provider.config.EmailAttribute),
|
||||
Phone: sr.Entries[0].GetAttributeValue(provider.config.PhoneAttribute),
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
for _, group := range sr.Entries[0].GetAttributeValues(provider.config.GroupMemberAttribute) {
|
||||
if group == provider.config.AdminLdapGroup {
|
||||
user.IsAdmin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (provider Provider) open() (*ldap.Conn, error) {
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
if provider.config.LdapCertConn {
|
||||
|
||||
certPlain, err := os.ReadFile(provider.config.LdapTlsCert)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load the certificate")
|
||||
|
||||
}
|
||||
|
||||
key, err := os.ReadFile(provider.config.LdapTlsKey)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load the key")
|
||||
}
|
||||
|
||||
certX509, err := tls.X509KeyPair(certPlain, key)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed X509")
|
||||
|
||||
}
|
||||
tlsConfig = &tls.Config{Certificates: []tls.Certificate{certX509}}
|
||||
|
||||
} else {
|
||||
|
||||
tlsConfig = &tls.Config{InsecureSkipVerify: !provider.config.CertValidation}
|
||||
}
|
||||
|
||||
conn, err := ldap.DialURL(provider.config.URL, ldap.DialWithTLSConfig(tlsConfig))
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to connect to LDAP")
|
||||
}
|
||||
|
||||
if provider.config.StartTLS {
|
||||
// Reconnect with TLS
|
||||
err = conn.StartTLS(tlsConfig)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to start TLS session")
|
||||
}
|
||||
}
|
||||
|
||||
err = conn.Bind(provider.config.BindUser, provider.config.BindPass)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to bind user")
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (provider Provider) close(conn *ldap.Conn) {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/authentication"
|
||||
"github.com/h44z/wg-portal/internal/common"
|
||||
"github.com/h44z/wg-portal/internal/users"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
|
||||
// Provider implements a password login method for a database backend.
|
||||
type Provider struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func New(cfg *common.DatabaseConfig) (*Provider, error) {
|
||||
p := &Provider{}
|
||||
|
||||
var err error
|
||||
p.db, err = common.GetDatabaseForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to setup authentication database %s", cfg.Database)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetName return provider name
|
||||
func (Provider) GetName() string {
|
||||
return string(users.UserSourceDatabase)
|
||||
}
|
||||
|
||||
// GetType return provider type
|
||||
func (Provider) GetType() authentication.AuthProviderType {
|
||||
return authentication.AuthProviderTypePassword
|
||||
}
|
||||
|
||||
// GetPriority return provider priority
|
||||
func (Provider) GetPriority() int {
|
||||
return 0 // DB password provider = highest prio
|
||||
}
|
||||
|
||||
func (provider Provider) SetupRoutes(_ *gin.RouterGroup) {
|
||||
// nothing here
|
||||
}
|
||||
|
||||
func (provider Provider) Login(ctx *authentication.AuthContext) (string, error) {
|
||||
username := strings.ToLower(ctx.Username)
|
||||
password := ctx.Password
|
||||
|
||||
// Validate input
|
||||
if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" {
|
||||
return "", errors.New("empty username or password")
|
||||
}
|
||||
|
||||
// Authenticate against the users database
|
||||
user := users.User{}
|
||||
provider.db.Where("email = ?", username).First(&user)
|
||||
|
||||
if user.Email == "" {
|
||||
return "", errors.New("invalid username")
|
||||
}
|
||||
|
||||
// Compare the stored hashed password, with the hashed version of the password that was received
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return "", errors.New("invalid password")
|
||||
}
|
||||
|
||||
return user.Email, nil
|
||||
}
|
||||
|
||||
func (provider Provider) Logout(_ *authentication.AuthContext) error {
|
||||
return nil // nothing here
|
||||
}
|
||||
|
||||
func (provider Provider) GetUserModel(ctx *authentication.AuthContext) (*authentication.User, error) {
|
||||
username := strings.ToLower(ctx.Username)
|
||||
|
||||
// Validate input
|
||||
if strings.Trim(username, " ") == "" {
|
||||
return nil, errors.New("empty username")
|
||||
}
|
||||
|
||||
// Fetch usermodel from users database
|
||||
user := users.User{}
|
||||
provider.db.Where("email = ?", username).First(&user)
|
||||
if user.Email != username {
|
||||
return nil, errors.New("invalid or disabled username")
|
||||
}
|
||||
|
||||
return &authentication.User{
|
||||
Email: user.Email,
|
||||
IsAdmin: user.IsAdmin,
|
||||
Firstname: user.Firstname,
|
||||
Lastname: user.Lastname,
|
||||
Phone: user.Phone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider Provider) InitializeAdmin(email, password string) error {
|
||||
email = strings.ToLower(email)
|
||||
if !emailRegex.MatchString(email) {
|
||||
return errors.New("admin username must be an email address")
|
||||
}
|
||||
|
||||
admin := users.User{}
|
||||
provider.db.Unscoped().Where("email = ?", email).FirstOrInit(&admin)
|
||||
|
||||
// newly created admin
|
||||
if admin.Email != email {
|
||||
// For security reasons a random admin password will be generated if the default one is still in use!
|
||||
if password == "wgportal" {
|
||||
password = generateRandomPassword()
|
||||
|
||||
fmt.Println("#############################################")
|
||||
fmt.Println("Administrator credentials:")
|
||||
fmt.Println(" Email: ", email)
|
||||
fmt.Println(" Password: ", password)
|
||||
fmt.Println()
|
||||
fmt.Println("This information will only be displayed once!")
|
||||
fmt.Println("#############################################")
|
||||
}
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to hash admin password")
|
||||
}
|
||||
|
||||
admin.Email = email
|
||||
admin.Password = users.PrivateString(hashedPassword)
|
||||
admin.Firstname = "WireGuard"
|
||||
admin.Lastname = "Administrator"
|
||||
admin.CreatedAt = time.Now()
|
||||
admin.UpdatedAt = time.Now()
|
||||
admin.IsAdmin = true
|
||||
admin.Source = users.UserSourceDatabase
|
||||
|
||||
res := provider.db.Create(admin)
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to create admin %s", admin.Email)
|
||||
}
|
||||
}
|
||||
|
||||
// update/reactivate
|
||||
if !admin.IsAdmin || admin.DeletedAt.Valid {
|
||||
// For security reasons a random admin password will be generated if the default one is still in use!
|
||||
if password == "wgportal" {
|
||||
password = generateRandomPassword()
|
||||
|
||||
fmt.Println("#############################################")
|
||||
fmt.Println("Administrator credentials:")
|
||||
fmt.Println(" Email: ", email)
|
||||
fmt.Println(" Password: ", password)
|
||||
fmt.Println()
|
||||
fmt.Println("This information will only be displayed once!")
|
||||
fmt.Println("#############################################")
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to hash admin password")
|
||||
}
|
||||
|
||||
admin.Password = users.PrivateString(hashedPassword)
|
||||
admin.IsAdmin = true
|
||||
admin.UpdatedAt = time.Now()
|
||||
|
||||
res := provider.db.Save(admin)
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to update admin %s", admin.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateRandomPassword() string {
|
||||
rand.Seed(time.Now().Unix())
|
||||
var randPassword strings.Builder
|
||||
charSet := "abcdedfghijklmnopqrstABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$"
|
||||
for i := 0; i < 12; i++ {
|
||||
random := rand.Intn(len(charSet))
|
||||
randPassword.WriteString(string(charSet[random]))
|
||||
}
|
||||
return randPassword.String()
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package authentication
|
||||
|
||||
// User represents the data that can be retrieved from authentication backends.
|
||||
type User struct {
|
||||
Email string
|
||||
IsAdmin bool
|
||||
|
||||
// optional fields
|
||||
Firstname string
|
||||
Lastname string
|
||||
Phone string
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, Migration{
|
||||
version: "1.0.7",
|
||||
migrateFn: func(db *gorm.DB) error {
|
||||
if err := db.Exec("UPDATE users SET email = LOWER(email)").Error; err != nil {
|
||||
return errors.Wrap(err, "failed to convert user emails to lower case")
|
||||
}
|
||||
if err := db.Exec("UPDATE peers SET email = LOWER(email)").Error; err != nil {
|
||||
return errors.Wrap(err, "failed to convert peer emails to lower case")
|
||||
}
|
||||
logrus.Infof("upgraded database format to version 1.0.7")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
migrations = append(migrations, Migration{
|
||||
version: "1.0.8",
|
||||
migrateFn: func(db *gorm.DB) error {
|
||||
logrus.Infof("upgraded database format to version 1.0.8")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
migrations = append(migrations, Migration{
|
||||
version: "1.0.9",
|
||||
migrateFn: func(db *gorm.DB) error {
|
||||
if db.Dialector.Name() != (sqlite.Dialector{}).Name() {
|
||||
logrus.Infof("upgraded database format to version 1.0.9")
|
||||
return nil // only perform migration for sqlite
|
||||
}
|
||||
|
||||
type sqlIndex struct {
|
||||
Name string `gorm:"column:name"`
|
||||
Table string `gorm:"column:tbl_name"`
|
||||
}
|
||||
var indices []sqlIndex
|
||||
if err := db.Raw("SELECT name, tbl_name FROM sqlite_master WHERE type == 'index'").Scan(&indices).Error; err != nil {
|
||||
return errors.Wrap(err, "failed to fetch indices")
|
||||
}
|
||||
|
||||
for _, index := range indices {
|
||||
if index.Table != "devices" && index.Table != "peers" && index.Table != "users" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(index.Name, "autoindex") {
|
||||
continue
|
||||
}
|
||||
if err := db.Exec("DROP INDEX " + index.Name).Error; err != nil {
|
||||
return errors.Wrap(err, "failed to drop index "+index.Name)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("upgraded database format to version 1.0.9")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type SupportedDatabase string
|
||||
|
||||
const (
|
||||
SupportedDatabaseMySQL SupportedDatabase = "mysql"
|
||||
SupportedDatabaseSQLite SupportedDatabase = "sqlite"
|
||||
)
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Typ SupportedDatabase `yaml:"typ" envconfig:"DATABASE_TYPE"` //mysql or sqlite
|
||||
Host string `yaml:"host" envconfig:"DATABASE_HOST"`
|
||||
Port int `yaml:"port" envconfig:"DATABASE_PORT"`
|
||||
Database string `yaml:"database" envconfig:"DATABASE_NAME"` // On SQLite: the database file-path, otherwise the database name
|
||||
User string `yaml:"user" envconfig:"DATABASE_USERNAME"`
|
||||
Password string `yaml:"password" envconfig:"DATABASE_PASSWORD"`
|
||||
}
|
||||
|
||||
func GetDatabaseForConfig(cfg *DatabaseConfig) (db *gorm.DB, err error) {
|
||||
switch cfg.Typ {
|
||||
case SupportedDatabaseSQLite:
|
||||
if _, err = os.Stat(filepath.Dir(cfg.Database)); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(filepath.Dir(cfg.Database), 0700); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
db, err = gorm.Open(sqlite.Open(cfg.Database), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case SupportedDatabaseMySQL:
|
||||
connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
|
||||
db, err = gorm.Open(mysql.Open(connectionString), &gorm.Config{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetConnMaxLifetime(time.Minute * 5)
|
||||
sqlDB.SetMaxIdleConns(2)
|
||||
sqlDB.SetMaxOpenConns(10)
|
||||
err = sqlDB.Ping() // This DOES open a connection if necessary. This makes sure the database is accessible
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to ping mysql authentication database")
|
||||
}
|
||||
}
|
||||
|
||||
// Enable Logger (logrus)
|
||||
logCfg := logger.Config{
|
||||
SlowThreshold: time.Second, // all slower than one second
|
||||
Colorful: false,
|
||||
LogLevel: logger.Silent, // default: log nothing
|
||||
}
|
||||
|
||||
if logrus.StandardLogger().GetLevel() == logrus.TraceLevel {
|
||||
logCfg.LogLevel = logger.Info
|
||||
logCfg.SlowThreshold = 500 * time.Millisecond // all slower than half a second
|
||||
}
|
||||
|
||||
db.Config.Logger = logger.New(logrus.StandardLogger(), logCfg)
|
||||
return
|
||||
}
|
||||
|
||||
type DatabaseMigrationInfo struct {
|
||||
Version string `gorm:"primaryKey"`
|
||||
Applied time.Time
|
||||
}
|
||||
|
||||
type Migration struct {
|
||||
version string
|
||||
migrateFn func(db *gorm.DB) error
|
||||
}
|
||||
|
||||
var migrations []Migration
|
||||
|
||||
func MigrateDatabase(db *gorm.DB, version string) error {
|
||||
if err := db.AutoMigrate(&DatabaseMigrationInfo{}); err != nil {
|
||||
return errors.Wrap(err, "failed to migrate version database")
|
||||
}
|
||||
|
||||
existingMigration := DatabaseMigrationInfo{}
|
||||
db.Where("version = ?", version).FirstOrInit(&existingMigration)
|
||||
|
||||
if existingMigration.Version == "" {
|
||||
lastVersion := DatabaseMigrationInfo{}
|
||||
db.Order("applied desc, version desc").FirstOrInit(&lastVersion)
|
||||
|
||||
if lastVersion.Version == "" {
|
||||
// fresh database, no migrations to apply
|
||||
res := db.Create(&DatabaseMigrationInfo{
|
||||
Version: version,
|
||||
Applied: time.Now(),
|
||||
})
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to write version %s to database", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Slice(migrations, func(i, j int) bool {
|
||||
return migrations[i].version < migrations[j].version
|
||||
})
|
||||
|
||||
for _, migration := range migrations {
|
||||
if migration.version > lastVersion.Version {
|
||||
if err := migration.migrateFn(db); err != nil {
|
||||
return errors.Wrapf(err, "failed to migrate to version %s", migration.version)
|
||||
}
|
||||
|
||||
res := db.Create(&DatabaseMigrationInfo{
|
||||
Version: migration.version,
|
||||
Applied: time.Now(),
|
||||
})
|
||||
if res.Error != nil {
|
||||
return errors.Wrapf(res.Error, "failed to write version %s to database", migration.version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
mail "github.com/xhit/go-simple-mail/v2"
|
||||
)
|
||||
|
||||
type MailEncryption string
|
||||
|
||||
const (
|
||||
MailEncryptionNone MailEncryption = "none"
|
||||
MailEncryptionTLS MailEncryption = "tls"
|
||||
MailEncryptionStartTLS MailEncryption = "starttls"
|
||||
)
|
||||
|
||||
type MailAuthType string
|
||||
|
||||
const (
|
||||
MailAuthPlain MailAuthType = "plain"
|
||||
MailAuthLogin MailAuthType = "login"
|
||||
MailAuthCramMD5 MailAuthType = "crammd5"
|
||||
)
|
||||
|
||||
type MailConfig struct {
|
||||
Host string `yaml:"host" envconfig:"EMAIL_HOST"`
|
||||
Port int `yaml:"port" envconfig:"EMAIL_PORT"`
|
||||
TLS bool `yaml:"tls" envconfig:"EMAIL_TLS"` // Deprecated, use MailConfig.Encryption instead.
|
||||
Encryption MailEncryption `yaml:"encryption" envconfig:"EMAIL_ENCRYPTION"`
|
||||
CertValidation bool `yaml:"certcheck" envconfig:"EMAIL_CERT_VALIDATION"`
|
||||
Username string `yaml:"user" envconfig:"EMAIL_USERNAME"`
|
||||
Password string `yaml:"pass" envconfig:"EMAIL_PASSWORD"`
|
||||
AuthType MailAuthType `yaml:"auth" envconfig:"EMAIL_AUTHTYPE"`
|
||||
}
|
||||
|
||||
type MailAttachment struct {
|
||||
Name string
|
||||
ContentType string
|
||||
Data io.Reader
|
||||
Embedded bool
|
||||
}
|
||||
|
||||
// SendEmailWithAttachments sends a mail with optional attachments.
|
||||
func SendEmailWithAttachments(cfg MailConfig, sender, replyTo, subject, body, htmlBody string, receivers []string, attachments []MailAttachment) error {
|
||||
srv := mail.NewSMTPClient()
|
||||
|
||||
srv.ConnectTimeout = 30 * time.Second
|
||||
srv.SendTimeout = 30 * time.Second
|
||||
srv.Host = cfg.Host
|
||||
srv.Port = cfg.Port
|
||||
srv.Username = cfg.Username
|
||||
srv.Password = cfg.Password
|
||||
|
||||
// TODO: remove this once the deprecated MailConfig.TLS config option has been removed
|
||||
if cfg.TLS {
|
||||
cfg.Encryption = MailEncryptionStartTLS
|
||||
}
|
||||
switch cfg.Encryption {
|
||||
case MailEncryptionTLS:
|
||||
srv.Encryption = mail.EncryptionSSLTLS
|
||||
case MailEncryptionStartTLS:
|
||||
srv.Encryption = mail.EncryptionSTARTTLS
|
||||
default: // MailEncryptionNone
|
||||
srv.Encryption = mail.EncryptionNone
|
||||
}
|
||||
srv.TLSConfig = &tls.Config{ServerName: srv.Host, InsecureSkipVerify: !cfg.CertValidation}
|
||||
switch cfg.AuthType {
|
||||
case MailAuthPlain:
|
||||
srv.Authentication = mail.AuthPlain
|
||||
case MailAuthLogin:
|
||||
srv.Authentication = mail.AuthLogin
|
||||
case MailAuthCramMD5:
|
||||
srv.Authentication = mail.AuthCRAMMD5
|
||||
}
|
||||
|
||||
client, err := srv.Connect()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to connect via SMTP")
|
||||
}
|
||||
|
||||
if replyTo == "" {
|
||||
replyTo = sender
|
||||
}
|
||||
|
||||
email := mail.NewMSG()
|
||||
email.SetFrom(sender).
|
||||
AddTo(receivers...).
|
||||
SetReplyTo(replyTo).
|
||||
SetSubject(subject)
|
||||
|
||||
email.SetBody(mail.TextHTML, htmlBody)
|
||||
email.AddAlternative(mail.TextPlain, body)
|
||||
|
||||
for _, attachment := range attachments {
|
||||
attachmentData, err := io.ReadAll(attachment.Data)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to read attachment data for %s", attachment.Name)
|
||||
}
|
||||
|
||||
if attachment.Embedded {
|
||||
email.AddInlineData(attachmentData, attachment.Name, attachment.ContentType)
|
||||
} else {
|
||||
email.AddAttachmentData(attachmentData, attachment.Name, attachment.ContentType)
|
||||
}
|
||||
}
|
||||
|
||||
// Call Send and pass the client
|
||||
err = email.Send(client)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to send email")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BroadcastAddr returns the last address in the given network, or the broadcast address.
|
||||
func BroadcastAddr(n *net.IPNet) net.IP {
|
||||
// The golang net package doesn't make it easy to calculate the broadcast address. :(
|
||||
var broadcast net.IP
|
||||
if len(n.IP) == 4 {
|
||||
broadcast = net.ParseIP("0.0.0.0").To4()
|
||||
} else {
|
||||
broadcast = net.ParseIP("::")
|
||||
}
|
||||
for i := 0; i < len(n.IP); i++ {
|
||||
broadcast[i] = n.IP[i] | ^n.Mask[i]
|
||||
}
|
||||
return broadcast
|
||||
}
|
||||
|
||||
// http://play.golang.org/p/m8TNTtygK0
|
||||
func IncreaseIP(ip net.IP) {
|
||||
for j := len(ip) - 1; j >= 0; j-- {
|
||||
ip[j]++
|
||||
if ip[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsIPv6 check if given ip is IPv6
|
||||
func IsIPv6(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() == nil
|
||||
}
|
||||
|
||||
// ParseStringList converts a comma separated string into a list of strings.
|
||||
// It also trims spaces from each element of the list.
|
||||
func ParseStringList(lst string) []string {
|
||||
tokens := strings.Split(lst, ",")
|
||||
validatedTokens := make([]string, 0, len(tokens))
|
||||
for i := range tokens {
|
||||
tokens[i] = strings.TrimSpace(tokens[i])
|
||||
if tokens[i] != "" {
|
||||
validatedTokens = append(validatedTokens, tokens[i])
|
||||
}
|
||||
}
|
||||
|
||||
return validatedTokens
|
||||
}
|
||||
|
||||
// ListToString converts a list of strings into a comma separated string.
|
||||
func ListToString(lst []string) string {
|
||||
return strings.Join(lst, ", ")
|
||||
}
|
||||
|
||||
// ListContains checks if a needle exists in the given list.
|
||||
func ListContains(lst []string, needle string) bool {
|
||||
for _, entry := range lst {
|
||||
if entry == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
|
||||
func ByteCountSI(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB",
|
||||
float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
func FormatDateHTML(t *time.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.Format("2006-01-02")
|
||||
}
|
118
internal/config/auth.go
Normal file
118
internal/config/auth.go
Normal file
@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
OpenIDConnect []OpenIDConnectProvider `yaml:"oidc"`
|
||||
OAuth []OAuthProvider `yaml:"oauth"`
|
||||
Ldap []LdapProvider `yaml:"ldap"`
|
||||
CallbackUrlPrefix string `yaml:"callback_url_prefix"`
|
||||
}
|
||||
|
||||
type BaseFields struct {
|
||||
UserIdentifier string `yaml:"user_identifier"`
|
||||
Email string `yaml:"email"`
|
||||
Firstname string `yaml:"firstname"`
|
||||
Lastname string `yaml:"lastname"`
|
||||
Phone string `yaml:"phone"`
|
||||
Department string `yaml:"department"`
|
||||
}
|
||||
|
||||
type OauthFields struct {
|
||||
BaseFields `yaml:",inline"`
|
||||
IsAdmin string `yaml:"is_admin"`
|
||||
}
|
||||
|
||||
type LdapFields struct {
|
||||
BaseFields `yaml:",inline"`
|
||||
GroupMembership string `yaml:"memberof"`
|
||||
}
|
||||
|
||||
type LdapProvider struct {
|
||||
// ProviderName is an internal name that is used to distinguish LDAP servers. It must not contain spaces or special characters.
|
||||
ProviderName string `yaml:"provider_name"`
|
||||
|
||||
URL string `yaml:"url"`
|
||||
StartTLS bool `yaml:"start_tls"`
|
||||
CertValidation bool `yaml:"cert_validation"`
|
||||
TlsCertificatePath string `yaml:"tls_certificate_path"`
|
||||
TlsKeyPath string `yaml:"tls_key_path"`
|
||||
|
||||
BaseDN string `yaml:"base_dn"`
|
||||
BindUser string `yaml:"bind_user"`
|
||||
BindPass string `yaml:"bind_pass"`
|
||||
|
||||
FieldMap LdapFields `yaml:"field_map"`
|
||||
|
||||
LoginFilter string `yaml:"login_filter"` // {{login_identifier}} gets replaced with the login email address / username
|
||||
AdminGroupDN string `yaml:"admin_group"` // Members of this group receive admin rights in WG-Portal
|
||||
ParsedAdminGroupDN *ldap.DN `yaml:"-"`
|
||||
|
||||
Synchronize bool `yaml:"synchronize"`
|
||||
// If DisableMissing is false, missing users will be deactivated
|
||||
DisableMissing bool `yaml:"deactivate_missing"`
|
||||
SyncFilter string `yaml:"sync_filter"`
|
||||
|
||||
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
|
||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||
}
|
||||
|
||||
type OpenIDConnectProvider struct {
|
||||
// ProviderName is an internal name that is used to distinguish oauth endpoints. It must not contain spaces or special characters.
|
||||
ProviderName string `yaml:"provider_name"`
|
||||
|
||||
// DisplayName is shown to the user on the login page. If it is empty, ProviderName will be displayed.
|
||||
DisplayName string `yaml:"display_name"`
|
||||
|
||||
BaseUrl string `yaml:"base_url"`
|
||||
|
||||
// ClientID is the application's ID.
|
||||
ClientID string `yaml:"client_id"`
|
||||
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string `yaml:"client_secret"`
|
||||
|
||||
// ExtraScopes specifies optional requested permissions.
|
||||
ExtraScopes []string `yaml:"extra_scopes"`
|
||||
|
||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||
FieldMap OauthFields `yaml:"field_map"`
|
||||
|
||||
// If RegistrationEnabled is set to true, missing users will be created in the database
|
||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||
}
|
||||
|
||||
type OAuthProvider struct {
|
||||
// ProviderName is an internal name that is used to distinguish oauth endpoints. It must not contain spaces or special characters.
|
||||
ProviderName string `yaml:"provider_name"`
|
||||
|
||||
// DisplayName is shown to the user on the login page. If it is empty, ProviderName will be displayed.
|
||||
DisplayName string `yaml:"display_name"`
|
||||
|
||||
BaseUrl string `yaml:"base_url"`
|
||||
|
||||
// ClientID is the application's ID.
|
||||
ClientID string `yaml:"client_id"`
|
||||
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string `yaml:"client_secret"`
|
||||
|
||||
AuthURL string `yaml:"auth_url"`
|
||||
TokenURL string `yaml:"token_url"`
|
||||
UserInfoURL string `yaml:"user_info_url"`
|
||||
|
||||
// RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
RedirectURL string `yaml:"redirect_url"`
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string `yaml:"scopes"`
|
||||
|
||||
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
|
||||
FieldMap OauthFields `yaml:"field_map"`
|
||||
|
||||
// If RegistrationEnabled is set to true, wg-portal will create new users that do not exist in the database.
|
||||
RegistrationEnabled bool `yaml:"registration_enabled"`
|
||||
}
|
89
internal/config/config.go
Normal file
89
internal/config/config.go
Normal file
@ -0,0 +1,89 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Core struct {
|
||||
// AdminUser defines the default administrator account that will be created
|
||||
AdminUser string `yaml:"admin_user"`
|
||||
AdminPassword string `yaml:"admin_password"`
|
||||
|
||||
EditableKeys bool `yaml:"editable_keys"`
|
||||
CreateDefaultPeer bool `yaml:"create_default_peer"`
|
||||
SelfProvisioningAllowed bool `yaml:"self_provisioning_allowed"`
|
||||
LdapSyncEnabled bool `yaml:"ldap_enabled"`
|
||||
ImportExisting bool `yaml:"import_existing"`
|
||||
RestoreState bool `yaml:"restore_state"`
|
||||
} `yaml:"core"`
|
||||
|
||||
Advanced struct {
|
||||
LogLevel string `yaml:"log_level"`
|
||||
StartupTimeout time.Duration `yaml:"startup_timeout"`
|
||||
LdapSyncInterval time.Duration `yaml:"ldap_sync_interval"`
|
||||
} `yaml:"advanced"`
|
||||
|
||||
Mail MailConfig `yaml:"mail"`
|
||||
|
||||
Auth Auth `yaml:"auth"`
|
||||
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
|
||||
Web WebConfig `yaml:"web"`
|
||||
}
|
||||
|
||||
func GetConfig() (*Config, error) {
|
||||
cfg := &Config{}
|
||||
|
||||
// default config
|
||||
|
||||
cfg.Core.ImportExisting = true
|
||||
cfg.Core.RestoreState = true
|
||||
|
||||
cfg.Database = DatabaseConfig{
|
||||
Type: "sqlite",
|
||||
DSN: "sqlite.db",
|
||||
}
|
||||
|
||||
cfg.Web = WebConfig{
|
||||
ListeningAddress: ":8888",
|
||||
SessionSecret: "verysecret",
|
||||
SessionIdentifier: "wgPortalSession",
|
||||
}
|
||||
|
||||
cfg.Auth.CallbackUrlPrefix = "/api/v0"
|
||||
|
||||
// override config values from YAML file
|
||||
|
||||
cfgFileName := "config.yml"
|
||||
if envCfgFileName := os.Getenv("WG_PORTAL_CONFIG"); envCfgFileName != "" {
|
||||
cfgFileName = envCfgFileName
|
||||
}
|
||||
|
||||
if err := loadConfigFile(cfg, cfgFileName); err != nil {
|
||||
return nil, fmt.Errorf("failed to load config from yaml: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func loadConfigFile(cfg any, filename string) error {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
decoder := yaml.NewDecoder(f)
|
||||
err = decoder.Decode(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
15
internal/config/database.go
Normal file
15
internal/config/database.go
Normal file
@ -0,0 +1,15 @@
|
||||
package config
|
||||
|
||||
type SupportedDatabase string
|
||||
|
||||
const (
|
||||
DatabaseMySQL SupportedDatabase = "mysql"
|
||||
DatabaseMsSQL SupportedDatabase = "mssql"
|
||||
DatabasePostgres SupportedDatabase = "postgres"
|
||||
DatabaseSQLite SupportedDatabase = "sqlite"
|
||||
)
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type SupportedDatabase `yaml:"type"`
|
||||
DSN string `yaml:"dsn"` // On SQLite: the database file-path, otherwise the dsn (see: https://gorm.io/docs/connecting_to_the_database.html)
|
||||
}
|
29
internal/config/mail.go
Normal file
29
internal/config/mail.go
Normal file
@ -0,0 +1,29 @@
|
||||
package config
|
||||
|
||||
type MailEncryption string
|
||||
|
||||
const (
|
||||
MailEncryptionNone MailEncryption = "none"
|
||||
MailEncryptionTLS MailEncryption = "tls"
|
||||
MailEncryptionStartTLS MailEncryption = "starttls"
|
||||
)
|
||||
|
||||
type MailAuthType string
|
||||
|
||||
const (
|
||||
MailAuthPlain MailAuthType = "plain"
|
||||
MailAuthLogin MailAuthType = "login"
|
||||
MailAuthCramMD5 MailAuthType = "crammd5"
|
||||
)
|
||||
|
||||
type MailConfig struct {
|
||||
Host string `envconfig:"EMAIL_HOST"`
|
||||
Port int `envconfig:"EMAIL_PORT"`
|
||||
Encryption MailEncryption `envconfig:"EMAIL_ENCRYPTION"`
|
||||
CertValidation bool `envconfig:"EMAIL_CERT_VALIDATION"`
|
||||
Username string `envconfig:"EMAIL_USERNAME"`
|
||||
Password string `envconfig:"EMAIL_PASSWORD"`
|
||||
AuthType MailAuthType `envconfig:"EMAIL_AUTHTYPE"`
|
||||
|
||||
From string `envconfig:"EMAIL_FROM"`
|
||||
}
|
11
internal/config/web.go
Normal file
11
internal/config/web.go
Normal file
@ -0,0 +1,11 @@
|
||||
package config
|
||||
|
||||
type WebConfig struct {
|
||||
ExternalUrl string `yaml:"external_url"`
|
||||
ListeningAddress string `yaml:"listening_address"`
|
||||
SessionIdentifier string `yaml:"session_identifier"`
|
||||
SessionSecret string `yaml:"session_secret"`
|
||||
CsrfSecret string `yaml:"csrf_secret"`
|
||||
SiteTitle string `yaml:"site_title"`
|
||||
SiteCompanyName string `yaml:"site_company_name"`
|
||||
}
|
51
internal/domain/auth.go
Normal file
51
internal/domain/auth.go
Normal file
@ -0,0 +1,51 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type LoginProvider string
|
||||
|
||||
type LoginProviderInfo struct {
|
||||
Identifier string
|
||||
Name string
|
||||
ProviderUrl string
|
||||
CallbackUrl string
|
||||
}
|
||||
|
||||
type AuthenticatorUserInfo struct {
|
||||
Identifier UserIdentifier
|
||||
Email string
|
||||
Firstname string
|
||||
Lastname string
|
||||
Phone string
|
||||
Department string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
type AuthenticatorType string
|
||||
|
||||
const (
|
||||
AuthenticatorTypeOAuth AuthenticatorType = "oauth"
|
||||
AuthenticatorTypeOidc AuthenticatorType = "oidc"
|
||||
)
|
||||
|
||||
type OauthAuthenticator interface {
|
||||
GetName() string
|
||||
GetType() AuthenticatorType
|
||||
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
|
||||
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
|
||||
GetUserInfo(ctx context.Context, token *oauth2.Token, nonce string) (map[string]interface{}, error)
|
||||
ParseUserInfo(raw map[string]interface{}) (*AuthenticatorUserInfo, error)
|
||||
RegistrationEnabled() bool
|
||||
}
|
||||
|
||||
type LdapAuthenticator interface {
|
||||
GetName() string
|
||||
PlaintextAuthentication(userId UserIdentifier, plainPassword string) error
|
||||
GetUserInfo(ctx context.Context, username UserIdentifier) (map[string]interface{}, error)
|
||||
ParseUserInfo(raw map[string]interface{}) (*AuthenticatorUserInfo, error)
|
||||
RegistrationEnabled() bool
|
||||
}
|
22
internal/domain/base.go
Normal file
22
internal/domain/base.go
Normal file
@ -0,0 +1,22 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseModel struct {
|
||||
CreatedBy string
|
||||
UpdatedBy string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type PrivateString string
|
||||
|
||||
func (PrivateString) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`""`), nil
|
||||
}
|
||||
|
||||
func (PrivateString) String() string {
|
||||
return ""
|
||||
}
|
53
internal/domain/context.go
Normal file
53
internal/domain/context.go
Normal file
@ -0,0 +1,53 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const CtxUserInfo = "userInfo"
|
||||
|
||||
type ContextUserInfo struct {
|
||||
Id UserIdentifier
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
func DefaultContextUserInfo() *ContextUserInfo {
|
||||
return &ContextUserInfo{
|
||||
Id: "_WG_SYS_UNKNOWN_",
|
||||
IsAdmin: false,
|
||||
}
|
||||
}
|
||||
|
||||
func SetUserInfoFromGin(c *gin.Context) context.Context {
|
||||
ginUserInfo, exists := c.Get(CtxUserInfo)
|
||||
|
||||
info := DefaultContextUserInfo()
|
||||
if exists {
|
||||
if ginInfo, ok := ginUserInfo.(*ContextUserInfo); ok {
|
||||
info = ginInfo
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(c.Request.Context(), CtxUserInfo, info)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
|
||||
ctx = context.WithValue(ctx, CtxUserInfo, info)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func GetUserInfo(ctx context.Context) *ContextUserInfo {
|
||||
rawInfo := ctx.Value(CtxUserInfo)
|
||||
if rawInfo == nil {
|
||||
return DefaultContextUserInfo()
|
||||
}
|
||||
|
||||
if info, ok := rawInfo.(*ContextUserInfo); ok {
|
||||
return info
|
||||
}
|
||||
|
||||
return DefaultContextUserInfo()
|
||||
}
|
67
internal/domain/crypto.go
Normal file
67
internal/domain/crypto.go
Normal file
@ -0,0 +1,67 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
PrivateKey string
|
||||
PublicKey string
|
||||
}
|
||||
|
||||
func (p KeyPair) GetPrivateKeyBytes() []byte {
|
||||
data, _ := base64.StdEncoding.DecodeString(p.PrivateKey)
|
||||
return data
|
||||
}
|
||||
|
||||
func (p KeyPair) GetPublicKeyBytes() []byte {
|
||||
data, _ := base64.StdEncoding.DecodeString(p.PublicKey)
|
||||
return data
|
||||
}
|
||||
|
||||
func (p KeyPair) GetPrivateKey() wgtypes.Key {
|
||||
key, _ := wgtypes.ParseKey(p.PrivateKey)
|
||||
return key
|
||||
}
|
||||
|
||||
func (p KeyPair) GetPublicKey() wgtypes.Key {
|
||||
key, _ := wgtypes.ParseKey(p.PublicKey)
|
||||
return key
|
||||
}
|
||||
|
||||
type PreSharedKey string
|
||||
|
||||
func NewFreshKeypair() (KeyPair, error) {
|
||||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return KeyPair{}, err
|
||||
}
|
||||
|
||||
return KeyPair{
|
||||
PrivateKey: privateKey.String(),
|
||||
PublicKey: privateKey.PublicKey().String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewPreSharedKey() (PreSharedKey, error) {
|
||||
preSharedKey, err := wgtypes.GenerateKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return PreSharedKey(preSharedKey.String()), nil
|
||||
}
|
||||
|
||||
func KeyBytesToString(key []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(key)
|
||||
}
|
||||
|
||||
func PublicKeyFromPrivateKey(key string) string {
|
||||
privKey, err := wgtypes.ParseKey(key)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return privKey.PublicKey().String()
|
||||
}
|
6
internal/domain/errors.go
Normal file
6
internal/domain/errors.go
Normal file
@ -0,0 +1,6 @@
|
||||
package domain
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotFound = errors.New("record not found")
|
||||
var ErrNotUnique = errors.New("record not unique")
|
151
internal/domain/interface.go
Normal file
151
internal/domain/interface.go
Normal file
@ -0,0 +1,151 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
InterfaceTypeServer InterfaceType = "server"
|
||||
InterfaceTypeClient InterfaceType = "client"
|
||||
InterfaceTypeAny InterfaceType = "any"
|
||||
)
|
||||
|
||||
type InterfaceIdentifier string
|
||||
type InterfaceType string
|
||||
|
||||
type Interface struct {
|
||||
BaseModel
|
||||
|
||||
// WireGuard specific (for the [interface] section of the config file)
|
||||
|
||||
Identifier InterfaceIdentifier `gorm:"primaryKey"` // device name, for example: wg0
|
||||
KeyPair // private/public Key of the server interface
|
||||
ListenPort int // the listening port, for example: 51820
|
||||
|
||||
Addresses []Cidr `gorm:"many2many:interface_addresses;"` // the interface ip addresses
|
||||
DnsStr string // the dns server that should be set if the interface is up, comma separated
|
||||
DnsSearchStr string // the dns search option string that should be set if the interface is up, will be appended to DnsStr
|
||||
|
||||
Mtu int // the device MTU
|
||||
FirewallMark int32 // a firewall mark
|
||||
RoutingTable string // the routing table
|
||||
|
||||
PreUp string // action that is executed before the device is up
|
||||
PostUp string // action that is executed after the device is up
|
||||
PreDown string // action that is executed before the device is down
|
||||
PostDown string // action that is executed after the device is down
|
||||
|
||||
SaveConfig bool // automatically persist config changes to the wgX.conf file
|
||||
|
||||
// WG Portal specific
|
||||
DisplayName string // a nice display name/ description for the interface
|
||||
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
|
||||
DriverType string // the interface driver type (linux, software, ...)
|
||||
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||
DisabledReason string // the reason why the interface has been disabled
|
||||
|
||||
// Default settings for the peer, used for new peers, those settings will be published to ConfigOption options of
|
||||
// the peer config
|
||||
|
||||
PeerDefNetworkStr string // the default subnets from which peers will get their IP addresses, comma seperated
|
||||
PeerDefDnsStr string // the default dns server for the peer
|
||||
PeerDefDnsSearchStr string // the default dns search options for the peer
|
||||
PeerDefEndpoint string // the default endpoint for the peer
|
||||
PeerDefAllowedIPsStr string // the default allowed IP string for the peer
|
||||
PeerDefMtu int // the default device MTU
|
||||
PeerDefPersistentKeepalive int // the default persistent keep-alive Value
|
||||
PeerDefFirewallMark int32 // default firewall mark
|
||||
PeerDefRoutingTable string // the default routing table
|
||||
|
||||
PeerDefPreUp string // default action that is executed before the device is up
|
||||
PeerDefPostUp string // default action that is executed after the device is up
|
||||
PeerDefPreDown string // default action that is executed before the device is down
|
||||
PeerDefPostDown string // default action that is executed after the device is down
|
||||
}
|
||||
|
||||
func (i *Interface) IsValid() bool {
|
||||
return true // TODO: implement check
|
||||
}
|
||||
|
||||
func (i *Interface) IsDisabled() bool {
|
||||
if i == nil {
|
||||
return true
|
||||
}
|
||||
return i.Disabled != nil
|
||||
}
|
||||
|
||||
func (i *Interface) AddressStr() string {
|
||||
return CidrsToString(i.Addresses)
|
||||
}
|
||||
|
||||
func (i *Interface) CopyCalculatedAttributes(src *Interface) {
|
||||
i.BaseModel = src.BaseModel
|
||||
}
|
||||
|
||||
type PhysicalInterface struct {
|
||||
Identifier InterfaceIdentifier // device name, for example: wg0
|
||||
KeyPair // private/public Key of the server interface
|
||||
ListenPort int // the listening port, for example: 51820
|
||||
|
||||
Addresses []Cidr // the interface ip addresses
|
||||
|
||||
Mtu int // the device MTU
|
||||
FirewallMark int32 // a firewall mark
|
||||
|
||||
DeviceUp bool // device status
|
||||
|
||||
ImportSource string // import source (wgctrl, file, ...)
|
||||
DeviceType string // device type (Linux kernel, userspace, ...)
|
||||
|
||||
BytesUpload uint64
|
||||
BytesDownload uint64
|
||||
}
|
||||
|
||||
func ConvertPhysicalInterface(pi *PhysicalInterface) *Interface {
|
||||
iface := &Interface{
|
||||
Identifier: pi.Identifier,
|
||||
KeyPair: pi.KeyPair,
|
||||
ListenPort: pi.ListenPort,
|
||||
Addresses: pi.Addresses,
|
||||
DnsStr: "",
|
||||
DnsSearchStr: "",
|
||||
Mtu: pi.Mtu,
|
||||
FirewallMark: pi.FirewallMark,
|
||||
RoutingTable: "",
|
||||
PreUp: "",
|
||||
PostUp: "",
|
||||
PreDown: "",
|
||||
PostDown: "",
|
||||
SaveConfig: false,
|
||||
DisplayName: string(pi.Identifier),
|
||||
Type: InterfaceTypeAny,
|
||||
DriverType: pi.DeviceType,
|
||||
Disabled: nil,
|
||||
PeerDefNetworkStr: "",
|
||||
PeerDefDnsStr: "",
|
||||
PeerDefDnsSearchStr: "",
|
||||
PeerDefEndpoint: "",
|
||||
PeerDefAllowedIPsStr: "",
|
||||
PeerDefMtu: pi.Mtu,
|
||||
PeerDefPersistentKeepalive: 0,
|
||||
PeerDefFirewallMark: 0,
|
||||
PeerDefRoutingTable: "",
|
||||
PeerDefPreUp: "",
|
||||
PeerDefPostUp: "",
|
||||
PeerDefPreDown: "",
|
||||
PeerDefPostDown: "",
|
||||
}
|
||||
|
||||
return iface
|
||||
}
|
||||
|
||||
func MergeToPhysicalInterface(pi *PhysicalInterface, i *Interface) {
|
||||
pi.Identifier = i.Identifier
|
||||
pi.PublicKey = i.PublicKey
|
||||
pi.PrivateKey = i.PrivateKey
|
||||
pi.ListenPort = i.ListenPort
|
||||
pi.Mtu = i.Mtu
|
||||
pi.FirewallMark = i.FirewallMark
|
||||
pi.DeviceUp = !i.IsDisabled()
|
||||
pi.Addresses = i.Addresses
|
||||
}
|
170
internal/domain/ip.go
Normal file
170
internal/domain/ip.go
Normal file
@ -0,0 +1,170 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/vishvananda/netlink"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Cidr struct {
|
||||
Cidr string `gorm:"primaryKey;column:cidr"` // Sqlite/GORM does not support composite primary keys...
|
||||
Addr string `gorm:"column:addr"`
|
||||
NetLength int `gorm:"column:net_len"`
|
||||
}
|
||||
|
||||
func (c Cidr) Prefix() netip.Prefix {
|
||||
return netip.PrefixFrom(netip.MustParseAddr(c.Addr), c.NetLength)
|
||||
}
|
||||
|
||||
func (c Cidr) String() string {
|
||||
return c.Prefix().String()
|
||||
}
|
||||
|
||||
func CidrFromString(str string) (Cidr, error) {
|
||||
prefix, err := netip.ParsePrefix(strings.TrimSpace(str))
|
||||
if err != nil {
|
||||
return Cidr{}, err
|
||||
}
|
||||
return CidrFromPrefix(prefix), nil
|
||||
}
|
||||
|
||||
func CidrsFromString(str string) ([]Cidr, error) {
|
||||
strParts := strings.Split(str, ",")
|
||||
cidrs := make([]Cidr, len(strParts))
|
||||
|
||||
for i, cidrStr := range strParts {
|
||||
cidr, err := CidrFromString(cidrStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cidrs[i] = cidr
|
||||
}
|
||||
|
||||
return cidrs, nil
|
||||
}
|
||||
|
||||
func CidrsMust(cidrs []Cidr, err error) []Cidr {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return cidrs
|
||||
}
|
||||
|
||||
func CidrsFromArray(strs []string) ([]Cidr, error) {
|
||||
cidrs := make([]Cidr, len(strs))
|
||||
|
||||
for i, cidrStr := range strs {
|
||||
cidr, err := CidrFromString(cidrStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cidrs[i] = cidr
|
||||
}
|
||||
|
||||
return cidrs, nil
|
||||
}
|
||||
|
||||
func CidrFromPrefix(prefix netip.Prefix) Cidr {
|
||||
return Cidr{
|
||||
Cidr: prefix.String(),
|
||||
Addr: prefix.Addr().String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
|
||||
func CidrFromIpNet(ipNet net.IPNet) Cidr {
|
||||
prefix, _ := CidrFromString(ipNet.String())
|
||||
return prefix
|
||||
}
|
||||
|
||||
func CidrFromNetlinkAddr(addr netlink.Addr) Cidr {
|
||||
prefix, _ := CidrFromString(addr.String())
|
||||
return prefix
|
||||
}
|
||||
|
||||
func (c Cidr) IpNet() *net.IPNet {
|
||||
_, cidr, _ := net.ParseCIDR(c.String())
|
||||
return cidr
|
||||
}
|
||||
|
||||
func (c Cidr) NetlinkAddr() *netlink.Addr {
|
||||
return &netlink.Addr{
|
||||
IPNet: c.IpNet(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Cidr) IsV4() bool {
|
||||
return c.Prefix().Addr().Is4()
|
||||
}
|
||||
|
||||
// BroadcastAddr returns the last address in the given network (for IPv6), or the broadcast address.
|
||||
func (c Cidr) BroadcastAddr() Cidr {
|
||||
prefix := c.Prefix()
|
||||
if !prefix.IsValid() {
|
||||
return Cidr{}
|
||||
}
|
||||
a16 := prefix.Addr().As16()
|
||||
var off uint8
|
||||
var bits uint8 = 128
|
||||
if prefix.Addr().Is4() {
|
||||
off = 12
|
||||
bits = 32
|
||||
}
|
||||
for b := uint8(prefix.Bits()); b < bits; b++ {
|
||||
byteNum, bitInByte := b/8, 7-(b%8)
|
||||
a16[off+byteNum] |= 1 << uint(bitInByte)
|
||||
}
|
||||
if prefix.Addr().Is4() {
|
||||
return Cidr{
|
||||
Addr: netip.AddrFrom16(a16).Unmap().String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
} else {
|
||||
return Cidr{
|
||||
Addr: netip.AddrFrom16(a16).String(), // doesn't unmap
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkAddr returns the network address in the given prefix.
|
||||
func (c Cidr) NetworkAddr() Cidr {
|
||||
prefix := c.Prefix()
|
||||
if !prefix.IsValid() {
|
||||
return Cidr{}
|
||||
}
|
||||
|
||||
return CidrFromPrefix(prefix.Masked())
|
||||
}
|
||||
|
||||
func (c Cidr) NextAddr() Cidr {
|
||||
prefix := c.Prefix()
|
||||
return Cidr{
|
||||
Addr: prefix.Addr().Next().String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Cidr) NextSubnet() Cidr {
|
||||
prefix := c.Prefix()
|
||||
return Cidr{
|
||||
Addr: c.BroadcastAddr().Prefix().Addr().Next().String(),
|
||||
NetLength: prefix.Bits(),
|
||||
}
|
||||
}
|
||||
|
||||
func CidrsToString(slice []Cidr) string {
|
||||
return strings.Join(CidrsToStringSlice(slice), ",")
|
||||
}
|
||||
|
||||
func CidrsToStringSlice(slice []Cidr) []string {
|
||||
cidrs := make([]string, len(slice))
|
||||
|
||||
for i, cidr := range slice {
|
||||
cidrs[i] = cidr.String()
|
||||
}
|
||||
|
||||
return cidrs
|
||||
}
|
204
internal/domain/ip_test.go
Normal file
204
internal/domain/ip_test.go
Normal file
@ -0,0 +1,204 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCidrFromString(t *testing.T) {
|
||||
type args struct {
|
||||
str string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Cidr
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "IPv4",
|
||||
args: args{str: "1.2.3.4/24"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.4/24")),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv4 Network",
|
||||
args: args{str: "1.2.3.0/24"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.0/24")),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv4 error",
|
||||
args: args{str: "1.1/24"},
|
||||
want: Cidr{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "IPv6 short",
|
||||
args: args{str: "fe00:1234::1/64"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00:1234::1/64")),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv6",
|
||||
args: args{str: "2A02:810A:900:333E:3B74:D237:E076:8B36/128"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("2A02:810A:900:333E:3B74:D237:E076:8B36/128")),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "IPv6 Network",
|
||||
args: args{str: "fe00::/56"},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00::/56")),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := CidrFromString(tt.args.str)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CidrFromString() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("CidrFromString() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCidr_BroadcastAddr(t *testing.T) {
|
||||
type fields struct {
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want Cidr
|
||||
}{
|
||||
{
|
||||
name: "V4",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.255/24")),
|
||||
},
|
||||
{
|
||||
name: "V6",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("fe00:d3ad:b33f:c0d3::/64")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00:d3ad:b33f:c0d3:ffff:ffff:ffff:ffff/64")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := CidrFromPrefix(tt.fields.Prefix)
|
||||
if got := c.BroadcastAddr(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("BroadcastAddr() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCidr_NetworkAddr(t *testing.T) {
|
||||
type fields struct {
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want Cidr
|
||||
}{
|
||||
|
||||
{
|
||||
name: "V4",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.0/24")),
|
||||
},
|
||||
{
|
||||
name: "V6",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("fe00:d3ad:b33f:c0d3::1234/64")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00:d3ad:b33f:c0d3::/64")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := CidrFromPrefix(tt.fields.Prefix)
|
||||
if got := c.NetworkAddr(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NetworkAddr() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCidr_NextAddr(t *testing.T) {
|
||||
type fields struct {
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want Cidr
|
||||
}{
|
||||
{
|
||||
name: "V4 normal",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.5/24")),
|
||||
},
|
||||
{
|
||||
name: "V4 broadcast",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.254/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.3.255/24")),
|
||||
},
|
||||
{
|
||||
name: "V4 overflow",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.255/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.4.0/24")),
|
||||
},
|
||||
{
|
||||
name: "V6 normal",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("fe00::1/64")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00::2/64")),
|
||||
},
|
||||
{
|
||||
name: "V6 overflow",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("fe00::ffff:ffff:ffff:ffff/64")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("fe00:0:0:1::/64")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := CidrFromPrefix(tt.fields.Prefix)
|
||||
if got := c.NextAddr(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NextAddr() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCidr_NextSubnet(t *testing.T) {
|
||||
type fields struct {
|
||||
Prefix netip.Prefix
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want Cidr
|
||||
}{
|
||||
{
|
||||
name: "V4",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/24")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.2.4.0/24")),
|
||||
},
|
||||
{
|
||||
name: "V4 bigger subnet",
|
||||
fields: fields{Prefix: netip.MustParsePrefix("1.2.3.4/16")},
|
||||
want: CidrFromPrefix(netip.MustParsePrefix("1.3.0.0/16")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := CidrFromPrefix(tt.fields.Prefix)
|
||||
if got := c.NextSubnet(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NextSubnet() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
18
internal/domain/mail.go
Normal file
18
internal/domain/mail.go
Normal file
@ -0,0 +1,18 @@
|
||||
package domain
|
||||
|
||||
import "io"
|
||||
|
||||
type MailOptions struct {
|
||||
ReplyTo string // defaults to the sender
|
||||
HtmlBody string // if html body is empty, a text-only email will be sent
|
||||
Cc []string
|
||||
Bcc []string
|
||||
Attachments []MailAttachment
|
||||
}
|
||||
|
||||
type MailAttachment struct {
|
||||
Name string
|
||||
ContentType string
|
||||
Data io.Reader
|
||||
Embedded bool
|
||||
}
|
113
internal/domain/options.go
Normal file
113
internal/domain/options.go
Normal file
@ -0,0 +1,113 @@
|
||||
package domain
|
||||
|
||||
type StringConfigOption struct {
|
||||
Value string `gorm:"column:v"`
|
||||
Overridable bool `gorm:"column:o"`
|
||||
}
|
||||
|
||||
func (o StringConfigOption) GetValue() string {
|
||||
return o.Value
|
||||
}
|
||||
|
||||
func (o *StringConfigOption) SetValue(value string) {
|
||||
o.Value = value
|
||||
}
|
||||
|
||||
func (o *StringConfigOption) TrySetValue(value string) bool {
|
||||
if o.Overridable {
|
||||
o.Value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewStringConfigOption(value string, overridable bool) StringConfigOption {
|
||||
return StringConfigOption{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
||||
|
||||
type IntConfigOption struct {
|
||||
Value int `gorm:"column:v"`
|
||||
Overridable bool `gorm:"column:o"`
|
||||
}
|
||||
|
||||
func (o IntConfigOption) GetValue() int {
|
||||
return o.Value
|
||||
}
|
||||
|
||||
func (o *IntConfigOption) SetValue(value int) {
|
||||
o.Value = value
|
||||
}
|
||||
|
||||
func (o *IntConfigOption) TrySetValue(value int) bool {
|
||||
if o.Overridable {
|
||||
o.Value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewIntConfigOption(value int, overridable bool) IntConfigOption {
|
||||
return IntConfigOption{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
||||
|
||||
type Int32ConfigOption struct {
|
||||
Value int32 `gorm:"column:v"`
|
||||
Overridable bool `gorm:"column:o"`
|
||||
}
|
||||
|
||||
func (o Int32ConfigOption) GetValue() int32 {
|
||||
return o.Value
|
||||
}
|
||||
|
||||
func (o *Int32ConfigOption) SetValue(value int32) {
|
||||
o.Value = value
|
||||
}
|
||||
|
||||
func (o *Int32ConfigOption) TrySetValue(value int32) bool {
|
||||
if o.Overridable {
|
||||
o.Value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewInt32ConfigOption(value int32, overridable bool) Int32ConfigOption {
|
||||
return Int32ConfigOption{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
||||
|
||||
type BoolConfigOption struct {
|
||||
Value bool `gorm:"column:v"`
|
||||
Overridable bool `gorm:"column:o"`
|
||||
}
|
||||
|
||||
func (o BoolConfigOption) GetValue() bool {
|
||||
return o.Value
|
||||
}
|
||||
|
||||
func (o *BoolConfigOption) SetValue(value bool) {
|
||||
o.Value = value
|
||||
}
|
||||
|
||||
func (o *BoolConfigOption) TrySetValue(value bool) bool {
|
||||
if o.Overridable {
|
||||
o.Value = value
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewBoolConfigOption(value bool, overridable bool) BoolConfigOption {
|
||||
return BoolConfigOption{
|
||||
Value: value,
|
||||
Overridable: overridable,
|
||||
}
|
||||
}
|
168
internal/domain/peer.go
Normal file
168
internal/domain/peer.go
Normal file
@ -0,0 +1,168 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type PeerIdentifier string
|
||||
|
||||
func (i PeerIdentifier) IsPublicKey() bool {
|
||||
_, err := wgtypes.ParseKey(string(i))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (i PeerIdentifier) ToPublicKey() wgtypes.Key {
|
||||
publicKey, _ := wgtypes.ParseKey(string(i))
|
||||
return publicKey
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
BaseModel
|
||||
|
||||
// WireGuard specific (for the [peer] section of the config file)
|
||||
|
||||
Endpoint StringConfigOption `gorm:"embedded;embeddedPrefix:endpoint_"` // the endpoint address
|
||||
EndpointPublicKey string `gorm:"column:endpoint_pubkey"` // the endpoint public key
|
||||
AllowedIPsStr StringConfigOption `gorm:"embedded;embeddedPrefix:allowed_ips_str_"` // all allowed ip subnets, comma seperated
|
||||
ExtraAllowedIPsStr string // all allowed ip subnets on the server side, comma seperated
|
||||
PresharedKey PreSharedKey // the pre-shared Key of the peer
|
||||
PersistentKeepalive IntConfigOption `gorm:"embedded;embeddedPrefix:persistent_keep_alive_"` // the persistent keep-alive interval
|
||||
|
||||
// WG Portal specific
|
||||
|
||||
DisplayName string // a nice display name/ description for the peer
|
||||
Identifier PeerIdentifier `gorm:"primaryKey;column:identifier"` // peer unique identifier
|
||||
UserIdentifier UserIdentifier `gorm:"index;column:user_identifier"` // the owner
|
||||
InterfaceIdentifier InterfaceIdentifier `gorm:"index;column:interface_identifier"` // the interface id
|
||||
Temporary *time.Time `gorm:"-"` // is this a temporary peer (only prepared, but never saved to db)
|
||||
Disabled *time.Time `gorm:"column:disabled"` // if this field is set, the peer is disabled
|
||||
DisabledReason string // the reason why the peer has been disabled
|
||||
ExpiresAt *time.Time `gorm:"column:expires_at"` // expiry dates for peers
|
||||
Notes string `form:"notes" binding:"omitempty"` // a note field for peers
|
||||
|
||||
// Interface settings for the peer, used to generate the [interface] section in the peer config file
|
||||
Interface PeerInterfaceConfig `gorm:"embedded"`
|
||||
}
|
||||
|
||||
func (p Peer) IsDisabled() bool {
|
||||
return p.Disabled != nil
|
||||
}
|
||||
|
||||
type PeerInterfaceConfig struct {
|
||||
KeyPair // private/public Key of the peer
|
||||
|
||||
Type InterfaceType `gorm:"column:iface_type"` // the interface type (server, client, any)
|
||||
|
||||
Addresses []Cidr `gorm:"many2many:peer_addresses;"` // the interface ip addresses
|
||||
DnsStr StringConfigOption `gorm:"embedded;embeddedPrefix:iface_dns_str_"` // the dns server that should be set if the interface is up, comma separated
|
||||
DnsSearchStr StringConfigOption `gorm:"embedded;embeddedPrefix:iface_dns_search_str_"` // the dns search option string that should be set if the interface is up, will be appended to DnsStr
|
||||
Mtu IntConfigOption `gorm:"embedded;embeddedPrefix:iface_mtu_"` // the device MTU
|
||||
FirewallMark Int32ConfigOption `gorm:"embedded;embeddedPrefix:iface_firewall_mark_"` // a firewall mark
|
||||
RoutingTable StringConfigOption `gorm:"embedded;embeddedPrefix:iface_routing_table_"` // the routing table
|
||||
|
||||
PreUp StringConfigOption `gorm:"embedded;embeddedPrefix:iface_pre_up_"` // action that is executed before the device is up
|
||||
PostUp StringConfigOption `gorm:"embedded;embeddedPrefix:iface_post_up_"` // action that is executed after the device is up
|
||||
PreDown StringConfigOption `gorm:"embedded;embeddedPrefix:iface_pre_down_"` // action that is executed before the device is down
|
||||
PostDown StringConfigOption `gorm:"embedded;embeddedPrefix:iface_post_down_"` // action that is executed after the device is down
|
||||
}
|
||||
|
||||
func (p *PeerInterfaceConfig) AddressStr() string {
|
||||
return CidrsToString(p.Addresses)
|
||||
}
|
||||
|
||||
type PhysicalPeer struct {
|
||||
Identifier PeerIdentifier // peer unique identifier
|
||||
|
||||
Endpoint string // the endpoint address
|
||||
AllowedIPs []Cidr // all allowed ip subnets
|
||||
KeyPair // private/public Key of the peer, for imports it only contains the public key as the private key is not known to the server
|
||||
PresharedKey PreSharedKey // the pre-shared Key of the peer
|
||||
PersistentKeepalive int // the persistent keep-alive interval
|
||||
|
||||
LastHandshake time.Time
|
||||
ProtocolVersion int
|
||||
|
||||
BytesUpload uint64
|
||||
BytesDownload uint64
|
||||
}
|
||||
|
||||
func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
||||
if p.PrivateKey == "" {
|
||||
return nil
|
||||
}
|
||||
key, err := wgtypes.ParseKey(p.PrivateKey)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &key
|
||||
}
|
||||
|
||||
func (p PhysicalPeer) GetEndpointAddress() *net.UDPAddr {
|
||||
if p.Endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
addr, err := net.ResolveUDPAddr("udp", p.Endpoint)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
func (p PhysicalPeer) GetPersistentKeepaliveTime() *time.Duration {
|
||||
if p.PersistentKeepalive == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keepAliveDuration := time.Duration(p.PersistentKeepalive) * time.Second
|
||||
return &keepAliveDuration
|
||||
}
|
||||
|
||||
func (p PhysicalPeer) GetAllowedIPs() ([]net.IPNet, error) {
|
||||
allowedIPs := make([]net.IPNet, len(p.AllowedIPs))
|
||||
for i, ip := range p.AllowedIPs {
|
||||
allowedIPs[i] = *ip.IpNet()
|
||||
}
|
||||
|
||||
return allowedIPs, nil
|
||||
}
|
||||
|
||||
func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
|
||||
peer := &Peer{
|
||||
Endpoint: StringConfigOption{Value: pp.Endpoint, Overridable: true},
|
||||
EndpointPublicKey: "",
|
||||
AllowedIPsStr: StringConfigOption{Value: "", Overridable: true},
|
||||
ExtraAllowedIPsStr: "",
|
||||
PresharedKey: pp.PresharedKey,
|
||||
PersistentKeepalive: IntConfigOption{Value: pp.PersistentKeepalive, Overridable: true},
|
||||
DisplayName: string(pp.Identifier),
|
||||
Identifier: pp.Identifier,
|
||||
UserIdentifier: "",
|
||||
InterfaceIdentifier: "",
|
||||
Temporary: nil,
|
||||
Disabled: nil,
|
||||
Interface: PeerInterfaceConfig{
|
||||
KeyPair: pp.KeyPair,
|
||||
},
|
||||
}
|
||||
|
||||
return peer
|
||||
}
|
||||
|
||||
func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
|
||||
pp.Identifier = p.Identifier
|
||||
pp.Endpoint = p.Endpoint.GetValue()
|
||||
allowedIPs, _ := CidrsFromString(p.AllowedIPsStr.GetValue())
|
||||
extraAllowedIPs, _ := CidrsFromString(p.ExtraAllowedIPsStr)
|
||||
pp.AllowedIPs = append(allowedIPs, extraAllowedIPs...)
|
||||
pp.PresharedKey = p.PresharedKey
|
||||
pp.PublicKey = p.Interface.PublicKey
|
||||
pp.PersistentKeepalive = p.PersistentKeepalive.GetValue()
|
||||
}
|
105
internal/domain/user.go
Normal file
105
internal/domain/user.go
Normal file
@ -0,0 +1,105 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
UserSourceLdap UserSource = "ldap" // LDAP / ActiveDirectory
|
||||
UserSourceDatabase UserSource = "db" // sqlite / mysql database
|
||||
UserSourceOauth UserSource = "oauth" // oauth / open id connect
|
||||
)
|
||||
|
||||
type UserIdentifier string
|
||||
|
||||
type UserSource string
|
||||
|
||||
// User is the user model that gets linked to peer entries, by default an empty user model with only the email address is created
|
||||
type User struct {
|
||||
BaseModel
|
||||
|
||||
// required fields
|
||||
Identifier UserIdentifier `gorm:"primaryKey;column:identifier"`
|
||||
Email string `form:"email" binding:"required,email"`
|
||||
Source UserSource
|
||||
ProviderName string
|
||||
IsAdmin bool
|
||||
|
||||
// optional fields
|
||||
Firstname string `form:"firstname" binding:"omitempty"`
|
||||
Lastname string `form:"lastname" binding:"omitempty"`
|
||||
Phone string `form:"phone" binding:"omitempty"`
|
||||
Department string `form:"department" binding:"omitempty"`
|
||||
Notes string `form:"notes" binding:"omitempty"`
|
||||
|
||||
// optional, integrated password authentication
|
||||
Password PrivateString `form:"password" binding:"omitempty"`
|
||||
Disabled *time.Time `gorm:"index;column:disabled"` // if this field is set, the user is disabled
|
||||
DisabledReason string // the reason why the user has been disabled
|
||||
Locked *time.Time `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login
|
||||
LockedReason string // the reason why the user has been locked
|
||||
|
||||
LinkedPeerCount int `gorm:"-"`
|
||||
}
|
||||
|
||||
func (u *User) IsDisabled() bool {
|
||||
return u.Disabled != nil
|
||||
}
|
||||
|
||||
func (u *User) CanChangePassword() error {
|
||||
if u.Source == UserSourceDatabase {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("password change only allowed for database source")
|
||||
}
|
||||
|
||||
func (u *User) EditAllowed() error {
|
||||
if u.Source == UserSourceDatabase {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("edit only allowed for database source")
|
||||
}
|
||||
|
||||
func (u *User) CheckPassword(password string) error {
|
||||
if u.Source != UserSourceDatabase {
|
||||
return errors.New("invalid user source")
|
||||
}
|
||||
|
||||
if u.Password == "" {
|
||||
return errors.New("empty user password")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)); err != nil {
|
||||
return errors.New("wrong password")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) HashPassword() error {
|
||||
if u.Password == "" {
|
||||
return nil // nothing to hash
|
||||
}
|
||||
|
||||
if _, err := bcrypt.Cost([]byte(u.Password)); err == nil {
|
||||
return nil // password already hashed
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = PrivateString(hash)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) CopyCalculatedAttributes(src *User) {
|
||||
u.BaseModel = src.BaseModel
|
||||
u.LinkedPeerCount = src.LinkedPeerCount
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
gldap "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type Type string
|
||||
|
||||
const (
|
||||
TypeActiveDirectory Type = "AD"
|
||||
TypeOpenLDAP Type = "OpenLDAP"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
URL string `yaml:"url" envconfig:"LDAP_URL"`
|
||||
StartTLS bool `yaml:"startTLS" envconfig:"LDAP_STARTTLS"`
|
||||
CertValidation bool `yaml:"certcheck" envconfig:"LDAP_CERT_VALIDATION"`
|
||||
BaseDN string `yaml:"dn" envconfig:"LDAP_BASEDN"`
|
||||
BindUser string `yaml:"user" envconfig:"LDAP_USER"`
|
||||
BindPass string `yaml:"pass" envconfig:"LDAP_PASSWORD"`
|
||||
|
||||
EmailAttribute string `yaml:"attrEmail" envconfig:"LDAP_ATTR_EMAIL"`
|
||||
FirstNameAttribute string `yaml:"attrFirstname" envconfig:"LDAP_ATTR_FIRSTNAME"`
|
||||
LastNameAttribute string `yaml:"attrLastname" envconfig:"LDAP_ATTR_LASTNAME"`
|
||||
PhoneAttribute string `yaml:"attrPhone" envconfig:"LDAP_ATTR_PHONE"`
|
||||
GroupMemberAttribute string `yaml:"attrGroups" envconfig:"LDAP_ATTR_GROUPS"`
|
||||
|
||||
LoginFilter string `yaml:"loginFilter" envconfig:"LDAP_LOGIN_FILTER"` // {{login_identifier}} gets replaced with the login email address
|
||||
SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"`
|
||||
SyncGroupFilter string `yaml:"syncGroupFilter" envconfig:"LDAP_SYNC_GROUP_FILTER"`
|
||||
AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal
|
||||
AdminLdapGroup_ *gldap.DN `yaml:"-"`
|
||||
EveryoneAdmin bool `yaml:"everyoneAdmin" envconfig:"LDAP_EVERYONE_ADMIN"`
|
||||
LdapCertConn bool `yaml:"ldapCertConn" envconfig:"LDAP_CERT_CONN"`
|
||||
LdapTlsCert string `yaml:"ldapTlsCert" envconfig:"LDAPTLS_CERT"`
|
||||
LdapTlsKey string `yaml:"ldapTlsKey" envconfig:"LDAPTLS_KEY"`
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"os"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ObjectType int
|
||||
|
||||
const (
|
||||
Users ObjectType = iota
|
||||
Groups
|
||||
)
|
||||
|
||||
type RawLdapData struct {
|
||||
DN string
|
||||
Attributes map[string]string
|
||||
RawAttributes map[string][][]byte
|
||||
}
|
||||
|
||||
func Open(cfg *Config) (*ldap.Conn, error) {
|
||||
var tlsConfig *tls.Config
|
||||
|
||||
if cfg.LdapCertConn {
|
||||
|
||||
certPlain, err := os.ReadFile(cfg.LdapTlsCert)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load the certificate")
|
||||
|
||||
}
|
||||
|
||||
key, err := os.ReadFile(cfg.LdapTlsKey)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to load the key")
|
||||
}
|
||||
|
||||
certX509, err := tls.X509KeyPair(certPlain, key)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed X509")
|
||||
|
||||
}
|
||||
tlsConfig = &tls.Config{Certificates: []tls.Certificate{certX509}}
|
||||
|
||||
} else {
|
||||
|
||||
tlsConfig = &tls.Config{InsecureSkipVerify: !cfg.CertValidation}
|
||||
}
|
||||
|
||||
conn, err := ldap.DialURL(cfg.URL, ldap.DialWithTLSConfig(tlsConfig))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to LDAP")
|
||||
}
|
||||
|
||||
if cfg.StartTLS {
|
||||
// Reconnect with TLS
|
||||
err = conn.StartTLS(tlsConfig)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to star TLS on connection")
|
||||
}
|
||||
}
|
||||
|
||||
err = conn.Bind(cfg.BindUser, cfg.BindPass)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to bind to LDAP")
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func Close(conn *ldap.Conn) {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) {
|
||||
client, err := Open(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to open ldap connection")
|
||||
}
|
||||
defer Close(client)
|
||||
|
||||
var searchRequest *ldap.SearchRequest
|
||||
var attrs []string
|
||||
|
||||
switch objType {
|
||||
case Users:
|
||||
// Search all users
|
||||
attrs = []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute,
|
||||
cfg.PhoneAttribute, cfg.GroupMemberAttribute}
|
||||
searchRequest = ldap.NewSearchRequest(
|
||||
cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
cfg.SyncFilter, attrs, nil,
|
||||
)
|
||||
case Groups:
|
||||
if cfg.SyncGroupFilter == "" {
|
||||
return nil, nil // no groups
|
||||
}
|
||||
// Search all groups
|
||||
attrs = []string{"dn", cfg.GroupMemberAttribute}
|
||||
searchRequest = ldap.NewSearchRequest(
|
||||
cfg.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
cfg.SyncGroupFilter, attrs, nil,
|
||||
)
|
||||
default:
|
||||
panic("invalid object type")
|
||||
}
|
||||
|
||||
sr, err := client.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to search in ldap")
|
||||
}
|
||||
|
||||
tmpData := make([]RawLdapData, 0, len(sr.Entries))
|
||||
|
||||
for _, entry := range sr.Entries {
|
||||
tmp := RawLdapData{
|
||||
DN: entry.DN,
|
||||
Attributes: make(map[string]string, len(attrs)),
|
||||
RawAttributes: make(map[string][][]byte, len(attrs)),
|
||||
}
|
||||
|
||||
for _, field := range attrs {
|
||||
tmp.Attributes[field] = entry.GetAttributeValue(field)
|
||||
tmp.RawAttributes[field] = entry.GetRawAttributeValues(field)
|
||||
}
|
||||
|
||||
tmpData = append(tmpData, tmp)
|
||||
}
|
||||
|
||||
return tmpData, nil
|
||||
}
|
6
internal/lowlevel/doc.go
Normal file
6
internal/lowlevel/doc.go
Normal file
@ -0,0 +1,6 @@
|
||||
package lowlevel
|
||||
|
||||
/**
|
||||
This package contains wrappers for low level api's like netlink or the WireGuard control library.
|
||||
Wrapping those external libraries makes mocking and testing code easier.
|
||||
*/
|
157
internal/lowlevel/mocks/NetlinkClient.go
Normal file
157
internal/lowlevel/mocks/NetlinkClient.go
Normal file
@ -0,0 +1,157 @@
|
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
netlink "github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// NetlinkClient is an autogenerated mock type for the NetlinkClient type
|
||||
type NetlinkClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AddrAdd provides a mock function with given fields: link, addr
|
||||
func (_m *NetlinkClient) AddrAdd(link netlink.Link, addr *netlink.Addr) error {
|
||||
ret := _m.Called(link, addr)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(netlink.Link, *netlink.Addr) error); ok {
|
||||
r0 = rf(link, addr)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// AddrList provides a mock function with given fields: link
|
||||
func (_m *NetlinkClient) AddrList(link netlink.Link) ([]netlink.Addr, error) {
|
||||
ret := _m.Called(link)
|
||||
|
||||
var r0 []netlink.Addr
|
||||
if rf, ok := ret.Get(0).(func(netlink.Link) []netlink.Addr); ok {
|
||||
r0 = rf(link)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]netlink.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(netlink.Link) error); ok {
|
||||
r1 = rf(link)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// AddrReplace provides a mock function with given fields: link, addr
|
||||
func (_m *NetlinkClient) AddrReplace(link netlink.Link, addr *netlink.Addr) error {
|
||||
ret := _m.Called(link, addr)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(netlink.Link, *netlink.Addr) error); ok {
|
||||
r0 = rf(link, addr)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LinkAdd provides a mock function with given fields: link
|
||||
func (_m *NetlinkClient) LinkAdd(link netlink.Link) error {
|
||||
ret := _m.Called(link)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(netlink.Link) error); ok {
|
||||
r0 = rf(link)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LinkByName provides a mock function with given fields: name
|
||||
func (_m *NetlinkClient) LinkByName(name string) (netlink.Link, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 netlink.Link
|
||||
if rf, ok := ret.Get(0).(func(string) netlink.Link); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(netlink.Link)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LinkDel provides a mock function with given fields: link
|
||||
func (_m *NetlinkClient) LinkDel(link netlink.Link) error {
|
||||
ret := _m.Called(link)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(netlink.Link) error); ok {
|
||||
r0 = rf(link)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LinkSetDown provides a mock function with given fields: link
|
||||
func (_m *NetlinkClient) LinkSetDown(link netlink.Link) error {
|
||||
ret := _m.Called(link)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(netlink.Link) error); ok {
|
||||
r0 = rf(link)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LinkSetMTU provides a mock function with given fields: link, mtu
|
||||
func (_m *NetlinkClient) LinkSetMTU(link netlink.Link, mtu int) error {
|
||||
ret := _m.Called(link, mtu)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(netlink.Link, int) error); ok {
|
||||
r0 = rf(link, mtu)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LinkSetUp provides a mock function with given fields: link
|
||||
func (_m *NetlinkClient) LinkSetUp(link netlink.Link) error {
|
||||
ret := _m.Called(link)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(netlink.Link) error); ok {
|
||||
r0 = rf(link)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
87
internal/lowlevel/mocks/WireGuardClient.go
Normal file
87
internal/lowlevel/mocks/WireGuardClient.go
Normal file
@ -0,0 +1,87 @@
|
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
wgtypes "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
// WireGuardClient is an autogenerated mock type for the WireGuardClient type
|
||||
type WireGuardClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *WireGuardClient) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConfigureDevice provides a mock function with given fields: name, cfg
|
||||
func (_m *WireGuardClient) ConfigureDevice(name string, cfg wgtypes.Config) error {
|
||||
ret := _m.Called(name, cfg)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, wgtypes.Config) error); ok {
|
||||
r0 = rf(name, cfg)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Device provides a mock function with given fields: name
|
||||
func (_m *WireGuardClient) Device(name string) (*wgtypes.Device, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *wgtypes.Device
|
||||
if rf, ok := ret.Get(0).(func(string) *wgtypes.Device); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*wgtypes.Device)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Devices provides a mock function with given fields:
|
||||
func (_m *WireGuardClient) Devices() ([]*wgtypes.Device, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*wgtypes.Device
|
||||
if rf, ok := ret.Get(0).(func() []*wgtypes.Device); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*wgtypes.Device)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
63
internal/lowlevel/netlink.go
Normal file
63
internal/lowlevel/netlink.go
Normal file
@ -0,0 +1,63 @@
|
||||
package lowlevel
|
||||
|
||||
import (
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// A NetlinkClient is a type which can control a netlink device.
|
||||
type NetlinkClient interface {
|
||||
LinkAdd(link netlink.Link) error
|
||||
LinkDel(link netlink.Link) error
|
||||
LinkByName(name string) (netlink.Link, error)
|
||||
LinkSetUp(link netlink.Link) error
|
||||
LinkSetDown(link netlink.Link) error
|
||||
LinkSetMTU(link netlink.Link, mtu int) error
|
||||
AddrReplace(link netlink.Link, addr *netlink.Addr) error
|
||||
AddrAdd(link netlink.Link, addr *netlink.Addr) error
|
||||
AddrList(link netlink.Link) ([]netlink.Addr, error)
|
||||
}
|
||||
|
||||
type NetlinkManager struct {
|
||||
}
|
||||
|
||||
func (n NetlinkManager) LinkAdd(link netlink.Link) error { return netlink.LinkAdd(link) }
|
||||
|
||||
func (n NetlinkManager) LinkDel(link netlink.Link) error { return netlink.LinkDel(link) }
|
||||
|
||||
func (n NetlinkManager) LinkByName(name string) (netlink.Link, error) {
|
||||
return netlink.LinkByName(name)
|
||||
}
|
||||
|
||||
func (n NetlinkManager) LinkSetUp(link netlink.Link) error { return netlink.LinkSetUp(link) }
|
||||
|
||||
func (n NetlinkManager) LinkSetDown(link netlink.Link) error { return netlink.LinkSetDown(link) }
|
||||
|
||||
func (n NetlinkManager) LinkSetMTU(link netlink.Link, mtu int) error {
|
||||
return netlink.LinkSetMTU(link, mtu)
|
||||
}
|
||||
|
||||
func (n NetlinkManager) AddrReplace(link netlink.Link, addr *netlink.Addr) error {
|
||||
return netlink.AddrReplace(link, addr)
|
||||
}
|
||||
|
||||
func (n NetlinkManager) AddrAdd(link netlink.Link, addr *netlink.Addr) error {
|
||||
return netlink.AddrAdd(link, addr)
|
||||
}
|
||||
|
||||
func (n NetlinkManager) AddrList(link netlink.Link) ([]netlink.Addr, error) {
|
||||
listIPv4, err := netlink.AddrList(link, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listIPv6, err := netlink.AddrList(link, netlink.FAMILY_V6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipAddresses := make([]netlink.Addr, 0, len(listIPv4)+len(listIPv6))
|
||||
ipAddresses = append(ipAddresses, listIPv4...)
|
||||
ipAddresses = append(ipAddresses, listIPv6...)
|
||||
|
||||
return ipAddresses, nil
|
||||
}
|
15
internal/lowlevel/wgctrl.go
Normal file
15
internal/lowlevel/wgctrl.go
Normal file
@ -0,0 +1,15 @@
|
||||
package lowlevel
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
// A WireGuardClient is a type which can control a WireGuard device.
|
||||
type WireGuardClient interface {
|
||||
io.Closer
|
||||
Devices() ([]*wgtypes.Device, error)
|
||||
Device(name string) (*wgtypes.Device, error)
|
||||
ConfigureDevice(name string, cfg wgtypes.Config) error
|
||||
}
|
70
internal/ports/api/build_tool/main.go
Normal file
70
internal/ports/api/build_tool/main.go
Normal file
@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/swaggo/swag"
|
||||
"github.com/swaggo/swag/gen"
|
||||
)
|
||||
|
||||
// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
|
||||
func main() {
|
||||
wd, err := os.Getwd() // should be the project root
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
apiBasePath := filepath.Join(wd, "/internal/ports/api")
|
||||
apis := []string{"v0"}
|
||||
|
||||
hasError := false
|
||||
for _, apiVersion := range apis {
|
||||
apiPath := filepath.Join(apiBasePath, apiVersion, "handlers")
|
||||
|
||||
apiVersion = strings.TrimLeft(apiVersion, "api-")
|
||||
log.Println("")
|
||||
log.Println("Generate swagger docs for API", apiVersion)
|
||||
log.Println("Api path:", apiPath)
|
||||
|
||||
err := generateApi(apiBasePath, apiPath, apiVersion)
|
||||
if err != nil {
|
||||
hasError = true
|
||||
logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err)
|
||||
}
|
||||
|
||||
log.Println("Generated swagger docs for API", apiVersion)
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func generateApi(basePath, apiPath, version string) error {
|
||||
err := gen.New().Build(&gen.Config{
|
||||
SearchDir: apiPath,
|
||||
Excludes: "",
|
||||
MainAPIFile: "base.go",
|
||||
PropNamingStrategy: swag.PascalCase,
|
||||
OutputDir: filepath.Join(basePath, "core/assets/doc"),
|
||||
OutputTypes: []string{"json", "yaml"},
|
||||
ParseVendor: false,
|
||||
ParseDependency: true,
|
||||
MarkdownFilesDir: "",
|
||||
ParseInternal: true,
|
||||
GeneratedTime: false,
|
||||
CodeExampleFilesDir: "",
|
||||
ParseDepth: 3,
|
||||
InstanceName: version,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("swag failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
21
internal/ports/api/core/assets.go
Normal file
21
internal/ports/api/core/assets.go
Normal file
@ -0,0 +1,21 @@
|
||||
package core
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed assets/tpl/*
|
||||
var apiTemplates embed.FS
|
||||
|
||||
//go:embed assets/css/*
|
||||
//go:embed assets/fonts/*
|
||||
//go:embed assets/img/*
|
||||
//go:embed assets/js/*
|
||||
//go:embed assets/doc/*
|
||||
var apiStatics embed.FS
|
||||
|
||||
//go:embed frontend-dist/assets/*
|
||||
//go:embed frontend-dist/img/*
|
||||
//go:embed frontend-dist/index.html
|
||||
//go:embed frontend-dist/favicon.ico
|
||||
//go:embed frontend-dist/favicon.png
|
||||
//go:embed frontend-dist/favicon-large.png
|
||||
var frontendStatics embed.FS
|
764
internal/ports/api/core/assets/doc/v0_swagger.json
Normal file
764
internal/ports/api/core/assets/doc/v0_swagger.json
Normal file
@ -0,0 +1,764 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "WireGuard Portal API - a testing API endpoint",
|
||||
"title": "WireGuard Portal API",
|
||||
"contact": {
|
||||
"name": "WireGuard Portal Developers",
|
||||
"url": "https://github.com/h44z/wg-portal"
|
||||
},
|
||||
"version": "0.0"
|
||||
},
|
||||
"basePath": "/api/v0",
|
||||
"paths": {
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Get all available external login providers.",
|
||||
"operationId": "auth_handleLoginPost",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Get all available external login providers.",
|
||||
"operationId": "auth_handleLogoutGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/providers": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Get all available external login providers.",
|
||||
"operationId": "auth_handleExternalLoginProvidersGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/session": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Get information about the currently logged-in user.",
|
||||
"operationId": "auth_handleSessionInfoGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.SessionInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/{provider}/callback": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Handle the OAuth callback.",
|
||||
"operationId": "auth_handleOauthCallbackGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/{provider}/init": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Initiate the OAuth login flow.",
|
||||
"operationId": "auth_handleOauthInitiateGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/frontend.js": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"text/javascript"
|
||||
],
|
||||
"tags": [
|
||||
"Configuration"
|
||||
],
|
||||
"summary": "Get the dynamic frontend configuration javascript.",
|
||||
"operationId": "config_handleConfigJsGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The JavaScript contents",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/csrf": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Security"
|
||||
],
|
||||
"summary": "Get a CSRF token for the current session.",
|
||||
"operationId": "base_handleCsrfGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/hostname": {
|
||||
"get": {
|
||||
"description": "Nothing more to describe...",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Testing"
|
||||
],
|
||||
"summary": "Get the current host name.",
|
||||
"operationId": "test_handleHostnameGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/interface/all": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Interface"
|
||||
],
|
||||
"summary": "Get all available interfaces.",
|
||||
"operationId": "interfaces_handleAllGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.Interface"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/interface/get/{id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Interface"
|
||||
],
|
||||
"summary": "Get single interface.",
|
||||
"operationId": "interfaces_handleSingleGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Interface"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/interface/peers/{id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Interface"
|
||||
],
|
||||
"summary": "Get peers for the given interface.",
|
||||
"operationId": "interfaces_handlePeersGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.Peer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/now": {
|
||||
"get": {
|
||||
"description": "Nothing more to describe...",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Testing"
|
||||
],
|
||||
"summary": "Get the current local time.",
|
||||
"operationId": "test_handleCurrentTimeGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/all/{id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Peer"
|
||||
],
|
||||
"summary": "Get peers for the given interface.",
|
||||
"operationId": "peers_handlePeersGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.Peer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get all user records.",
|
||||
"operationId": "users_handleAllGet",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/model.User"
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"model.Error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.Interface": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Addresses": {
|
||||
"description": "the interface ip addresses",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Disabled": {
|
||||
"description": "flag that specifies if the interface is enabled (up) or not (down)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"DisabledReason": {
|
||||
"description": "the reason why the interface has been disabled",
|
||||
"type": "string"
|
||||
},
|
||||
"DisplayName": {
|
||||
"description": "a nice display name/ description for the interface",
|
||||
"type": "string"
|
||||
},
|
||||
"Dns": {
|
||||
"description": "the dns server that should be set if the interface is up, comma separated",
|
||||
"type": "string"
|
||||
},
|
||||
"DnsSearch": {
|
||||
"description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
|
||||
"type": "string"
|
||||
},
|
||||
"EnabledPeers": {
|
||||
"type": "integer"
|
||||
},
|
||||
"FirewallMark": {
|
||||
"description": "a firewall mark",
|
||||
"type": "integer"
|
||||
},
|
||||
"Identifier": {
|
||||
"description": "device name, for example: wg0",
|
||||
"type": "string",
|
||||
"example": "wg0"
|
||||
},
|
||||
"ListenPort": {
|
||||
"description": "the listening port, for example: 51820",
|
||||
"type": "integer"
|
||||
},
|
||||
"Mode": {
|
||||
"description": "the interface type, either 'server', 'client' or 'any'",
|
||||
"type": "string",
|
||||
"example": "server"
|
||||
},
|
||||
"Mtu": {
|
||||
"description": "the device MTU",
|
||||
"type": "integer"
|
||||
},
|
||||
"PeerDefAllowedIPs": {
|
||||
"description": "the default allowed IP string for the peer",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefDns": {
|
||||
"description": "the default dns server for the peer",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefDnsSearch": {
|
||||
"description": "the default dns search options for the peer",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefEndpoint": {
|
||||
"description": "the default endpoint for the peer",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefFirewallMark": {
|
||||
"description": "default firewall mark",
|
||||
"type": "integer"
|
||||
},
|
||||
"PeerDefMtu": {
|
||||
"description": "the default device MTU",
|
||||
"type": "integer"
|
||||
},
|
||||
"PeerDefNetwork": {
|
||||
"description": "the default subnets from which peers will get their IP addresses, comma seperated",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefPersistentKeepalive": {
|
||||
"description": "the default persistent keep-alive Value",
|
||||
"type": "integer"
|
||||
},
|
||||
"PeerDefPostDown": {
|
||||
"description": "default action that is executed after the device is down",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefPostUp": {
|
||||
"description": "default action that is executed after the device is up",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefPreDown": {
|
||||
"description": "default action that is executed before the device is down",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefPreUp": {
|
||||
"description": "default action that is executed before the device is up",
|
||||
"type": "string"
|
||||
},
|
||||
"PeerDefRoutingTable": {
|
||||
"description": "the default routing table",
|
||||
"type": "string"
|
||||
},
|
||||
"PostDown": {
|
||||
"description": "action that is executed after the device is down",
|
||||
"type": "string"
|
||||
},
|
||||
"PostUp": {
|
||||
"description": "action that is executed after the device is up",
|
||||
"type": "string"
|
||||
},
|
||||
"PreDown": {
|
||||
"description": "action that is executed before the device is down",
|
||||
"type": "string"
|
||||
},
|
||||
"PreUp": {
|
||||
"description": "action that is executed before the device is up",
|
||||
"type": "string"
|
||||
},
|
||||
"PrivateKey": {
|
||||
"description": "private Key of the server interface",
|
||||
"type": "string",
|
||||
"example": "abcdef=="
|
||||
},
|
||||
"PublicKey": {
|
||||
"description": "public Key of the server interface",
|
||||
"type": "string",
|
||||
"example": "abcdef=="
|
||||
},
|
||||
"RoutingTable": {
|
||||
"description": "the routing table",
|
||||
"type": "string"
|
||||
},
|
||||
"SaveConfig": {
|
||||
"description": "automatically persist config changes to the wgX.conf file",
|
||||
"type": "boolean"
|
||||
},
|
||||
"TotalPeers": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.LoginProviderInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"CallbackUrl": {
|
||||
"type": "string",
|
||||
"example": "/auth/google/callback"
|
||||
},
|
||||
"Identifier": {
|
||||
"type": "string",
|
||||
"example": "google"
|
||||
},
|
||||
"Name": {
|
||||
"type": "string",
|
||||
"example": "Login with Google"
|
||||
},
|
||||
"ProviderUrl": {
|
||||
"type": "string",
|
||||
"example": "/auth/google/login"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.Peer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Addresses": {
|
||||
"description": "the interface ip addresses",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"AllowedIPs": {
|
||||
"description": "all allowed ip subnets, comma seperated",
|
||||
"type": "string"
|
||||
},
|
||||
"Disabled": {
|
||||
"description": "flag that specifies if the peer is enabled (up) or not (down)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"DisabledReason": {
|
||||
"description": "the reason why the peer has been disabled",
|
||||
"type": "string"
|
||||
},
|
||||
"DisplayName": {
|
||||
"description": "a nice display name/ description for the peer",
|
||||
"type": "string"
|
||||
},
|
||||
"Dns": {
|
||||
"description": "the dns server that should be set if the interface is up, comma separated",
|
||||
"type": "string"
|
||||
},
|
||||
"DnsSearch": {
|
||||
"description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
|
||||
"type": "string"
|
||||
},
|
||||
"Endpoint": {
|
||||
"description": "the endpoint address",
|
||||
"type": "string"
|
||||
},
|
||||
"EndpointPublicKey": {
|
||||
"description": "the endpoint public key",
|
||||
"type": "string"
|
||||
},
|
||||
"ExtraAllowedIPs": {
|
||||
"description": "all allowed ip subnets on the server side, comma seperated",
|
||||
"type": "string"
|
||||
},
|
||||
"FirewallMark": {
|
||||
"description": "a firewall mark",
|
||||
"type": "integer"
|
||||
},
|
||||
"Identifier": {
|
||||
"description": "peer unique identifier",
|
||||
"type": "string",
|
||||
"example": "super_nice_peer"
|
||||
},
|
||||
"InterfaceIdentifier": {
|
||||
"description": "the interface id",
|
||||
"type": "string"
|
||||
},
|
||||
"Mode": {
|
||||
"description": "the peer interface type (server, client, any)",
|
||||
"type": "string"
|
||||
},
|
||||
"Mtu": {
|
||||
"description": "the device MTU",
|
||||
"type": "integer"
|
||||
},
|
||||
"PersistentKeepalive": {
|
||||
"description": "the persistent keep-alive interval",
|
||||
"type": "integer"
|
||||
},
|
||||
"PostDown": {
|
||||
"description": "action that is executed after the device is down",
|
||||
"type": "string"
|
||||
},
|
||||
"PostUp": {
|
||||
"description": "action that is executed after the device is up",
|
||||
"type": "string"
|
||||
},
|
||||
"PreDown": {
|
||||
"description": "action that is executed before the device is down",
|
||||
"type": "string"
|
||||
},
|
||||
"PreUp": {
|
||||
"description": "action that is executed before the device is up",
|
||||
"type": "string"
|
||||
},
|
||||
"PresharedKey": {
|
||||
"description": "the pre-shared Key of the peer",
|
||||
"type": "string"
|
||||
},
|
||||
"PrivateKey": {
|
||||
"description": "private Key of the server peer",
|
||||
"type": "string",
|
||||
"example": "abcdef=="
|
||||
},
|
||||
"PublicKey": {
|
||||
"description": "public Key of the server peer",
|
||||
"type": "string",
|
||||
"example": "abcdef=="
|
||||
},
|
||||
"RoutingTable": {
|
||||
"description": "the routing table",
|
||||
"type": "string"
|
||||
},
|
||||
"UserIdentifier": {
|
||||
"description": "the owner",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.SessionInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"IsAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"LoggedIn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"UserEmail": {
|
||||
"type": "string"
|
||||
},
|
||||
"UserFirstname": {
|
||||
"type": "string"
|
||||
},
|
||||
"UserIdentifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"UserLastname": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Department": {
|
||||
"type": "string"
|
||||
},
|
||||
"Disabled": {
|
||||
"description": "if this field is set, the user is disabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"DisabledReason": {
|
||||
"description": "the reason why the user has been disabled",
|
||||
"type": "string"
|
||||
},
|
||||
"Email": {
|
||||
"type": "string"
|
||||
},
|
||||
"Firstname": {
|
||||
"type": "string"
|
||||
},
|
||||
"Identifier": {
|
||||
"type": "string"
|
||||
},
|
||||
"IsAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Lastname": {
|
||||
"type": "string"
|
||||
},
|
||||
"Notes": {
|
||||
"type": "string"
|
||||
},
|
||||
"Password": {
|
||||
"type": "string"
|
||||
},
|
||||
"PeerCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"Phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"ProviderName": {
|
||||
"type": "string"
|
||||
},
|
||||
"Source": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
531
internal/ports/api/core/assets/doc/v0_swagger.yaml
Normal file
531
internal/ports/api/core/assets/doc/v0_swagger.yaml
Normal file
@ -0,0 +1,531 @@
|
||||
basePath: /api/v0
|
||||
definitions:
|
||||
model.Error:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
model.Interface:
|
||||
properties:
|
||||
Addresses:
|
||||
description: the interface ip addresses
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
Disabled:
|
||||
description: flag that specifies if the interface is enabled (up) or not (down)
|
||||
type: boolean
|
||||
DisabledReason:
|
||||
description: the reason why the interface has been disabled
|
||||
type: string
|
||||
DisplayName:
|
||||
description: a nice display name/ description for the interface
|
||||
type: string
|
||||
Dns:
|
||||
description: the dns server that should be set if the interface is up, comma
|
||||
separated
|
||||
type: string
|
||||
DnsSearch:
|
||||
description: the dns search option string that should be set if the interface
|
||||
is up, will be appended to DnsStr
|
||||
type: string
|
||||
EnabledPeers:
|
||||
type: integer
|
||||
FirewallMark:
|
||||
description: a firewall mark
|
||||
type: integer
|
||||
Identifier:
|
||||
description: 'device name, for example: wg0'
|
||||
example: wg0
|
||||
type: string
|
||||
ListenPort:
|
||||
description: 'the listening port, for example: 51820'
|
||||
type: integer
|
||||
Mode:
|
||||
description: the interface type, either 'server', 'client' or 'any'
|
||||
example: server
|
||||
type: string
|
||||
Mtu:
|
||||
description: the device MTU
|
||||
type: integer
|
||||
PeerDefAllowedIPs:
|
||||
description: the default allowed IP string for the peer
|
||||
type: string
|
||||
PeerDefDns:
|
||||
description: the default dns server for the peer
|
||||
type: string
|
||||
PeerDefDnsSearch:
|
||||
description: the default dns search options for the peer
|
||||
type: string
|
||||
PeerDefEndpoint:
|
||||
description: the default endpoint for the peer
|
||||
type: string
|
||||
PeerDefFirewallMark:
|
||||
description: default firewall mark
|
||||
type: integer
|
||||
PeerDefMtu:
|
||||
description: the default device MTU
|
||||
type: integer
|
||||
PeerDefNetwork:
|
||||
description: the default subnets from which peers will get their IP addresses,
|
||||
comma seperated
|
||||
type: string
|
||||
PeerDefPersistentKeepalive:
|
||||
description: the default persistent keep-alive Value
|
||||
type: integer
|
||||
PeerDefPostDown:
|
||||
description: default action that is executed after the device is down
|
||||
type: string
|
||||
PeerDefPostUp:
|
||||
description: default action that is executed after the device is up
|
||||
type: string
|
||||
PeerDefPreDown:
|
||||
description: default action that is executed before the device is down
|
||||
type: string
|
||||
PeerDefPreUp:
|
||||
description: default action that is executed before the device is up
|
||||
type: string
|
||||
PeerDefRoutingTable:
|
||||
description: the default routing table
|
||||
type: string
|
||||
PostDown:
|
||||
description: action that is executed after the device is down
|
||||
type: string
|
||||
PostUp:
|
||||
description: action that is executed after the device is up
|
||||
type: string
|
||||
PreDown:
|
||||
description: action that is executed before the device is down
|
||||
type: string
|
||||
PreUp:
|
||||
description: action that is executed before the device is up
|
||||
type: string
|
||||
PrivateKey:
|
||||
description: private Key of the server interface
|
||||
example: abcdef==
|
||||
type: string
|
||||
PublicKey:
|
||||
description: public Key of the server interface
|
||||
example: abcdef==
|
||||
type: string
|
||||
RoutingTable:
|
||||
description: the routing table
|
||||
type: string
|
||||
SaveConfig:
|
||||
description: automatically persist config changes to the wgX.conf file
|
||||
type: boolean
|
||||
TotalPeers:
|
||||
type: integer
|
||||
type: object
|
||||
model.LoginProviderInfo:
|
||||
properties:
|
||||
CallbackUrl:
|
||||
example: /auth/google/callback
|
||||
type: string
|
||||
Identifier:
|
||||
example: google
|
||||
type: string
|
||||
Name:
|
||||
example: Login with Google
|
||||
type: string
|
||||
ProviderUrl:
|
||||
example: /auth/google/login
|
||||
type: string
|
||||
type: object
|
||||
model.Peer:
|
||||
properties:
|
||||
Addresses:
|
||||
description: the interface ip addresses
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
AllowedIPs:
|
||||
description: all allowed ip subnets, comma seperated
|
||||
type: string
|
||||
Disabled:
|
||||
description: flag that specifies if the peer is enabled (up) or not (down)
|
||||
type: boolean
|
||||
DisabledReason:
|
||||
description: the reason why the peer has been disabled
|
||||
type: string
|
||||
DisplayName:
|
||||
description: a nice display name/ description for the peer
|
||||
type: string
|
||||
Dns:
|
||||
description: the dns server that should be set if the interface is up, comma
|
||||
separated
|
||||
type: string
|
||||
DnsSearch:
|
||||
description: the dns search option string that should be set if the interface
|
||||
is up, will be appended to DnsStr
|
||||
type: string
|
||||
Endpoint:
|
||||
description: the endpoint address
|
||||
type: string
|
||||
EndpointPublicKey:
|
||||
description: the endpoint public key
|
||||
type: string
|
||||
ExtraAllowedIPs:
|
||||
description: all allowed ip subnets on the server side, comma seperated
|
||||
type: string
|
||||
FirewallMark:
|
||||
description: a firewall mark
|
||||
type: integer
|
||||
Identifier:
|
||||
description: peer unique identifier
|
||||
example: super_nice_peer
|
||||
type: string
|
||||
InterfaceIdentifier:
|
||||
description: the interface id
|
||||
type: string
|
||||
Mode:
|
||||
description: the peer interface type (server, client, any)
|
||||
type: string
|
||||
Mtu:
|
||||
description: the device MTU
|
||||
type: integer
|
||||
PersistentKeepalive:
|
||||
description: the persistent keep-alive interval
|
||||
type: integer
|
||||
PostDown:
|
||||
description: action that is executed after the device is down
|
||||
type: string
|
||||
PostUp:
|
||||
description: action that is executed after the device is up
|
||||
type: string
|
||||
PreDown:
|
||||
description: action that is executed before the device is down
|
||||
type: string
|
||||
PreUp:
|
||||
description: action that is executed before the device is up
|
||||
type: string
|
||||
PresharedKey:
|
||||
description: the pre-shared Key of the peer
|
||||
type: string
|
||||
PrivateKey:
|
||||
description: private Key of the server peer
|
||||
example: abcdef==
|
||||
type: string
|
||||
PublicKey:
|
||||
description: public Key of the server peer
|
||||
example: abcdef==
|
||||
type: string
|
||||
RoutingTable:
|
||||
description: the routing table
|
||||
type: string
|
||||
UserIdentifier:
|
||||
description: the owner
|
||||
type: string
|
||||
type: object
|
||||
model.SessionInfo:
|
||||
properties:
|
||||
IsAdmin:
|
||||
type: boolean
|
||||
LoggedIn:
|
||||
type: boolean
|
||||
UserEmail:
|
||||
type: string
|
||||
UserFirstname:
|
||||
type: string
|
||||
UserIdentifier:
|
||||
type: string
|
||||
UserLastname:
|
||||
type: string
|
||||
type: object
|
||||
model.User:
|
||||
properties:
|
||||
Department:
|
||||
type: string
|
||||
Disabled:
|
||||
description: if this field is set, the user is disabled
|
||||
type: boolean
|
||||
DisabledReason:
|
||||
description: the reason why the user has been disabled
|
||||
type: string
|
||||
Email:
|
||||
type: string
|
||||
Firstname:
|
||||
type: string
|
||||
Identifier:
|
||||
type: string
|
||||
IsAdmin:
|
||||
type: boolean
|
||||
Lastname:
|
||||
type: string
|
||||
Notes:
|
||||
type: string
|
||||
Password:
|
||||
type: string
|
||||
PeerCount:
|
||||
type: integer
|
||||
Phone:
|
||||
type: string
|
||||
ProviderName:
|
||||
type: string
|
||||
Source:
|
||||
type: string
|
||||
type: object
|
||||
info:
|
||||
contact:
|
||||
name: WireGuard Portal Developers
|
||||
url: https://github.com/h44z/wg-portal
|
||||
description: WireGuard Portal API - a testing API endpoint
|
||||
title: WireGuard Portal API
|
||||
version: "0.0"
|
||||
paths:
|
||||
/auth/{provider}/callback:
|
||||
get:
|
||||
operationId: auth_handleOauthCallbackGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Handle the OAuth callback.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/{provider}/init:
|
||||
get:
|
||||
operationId: auth_handleOauthInitiateGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Initiate the OAuth login flow.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/login:
|
||||
post:
|
||||
operationId: auth_handleLoginPost
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Get all available external login providers.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/logout:
|
||||
get:
|
||||
operationId: auth_handleLogoutGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Get all available external login providers.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/providers:
|
||||
get:
|
||||
operationId: auth_handleExternalLoginProvidersGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.LoginProviderInfo'
|
||||
type: array
|
||||
summary: Get all available external login providers.
|
||||
tags:
|
||||
- Authentication
|
||||
/auth/session:
|
||||
get:
|
||||
operationId: auth_handleSessionInfoGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.SessionInfo'
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get information about the currently logged-in user.
|
||||
tags:
|
||||
- Authentication
|
||||
/config/frontend.js:
|
||||
get:
|
||||
operationId: config_handleConfigJsGet
|
||||
produces:
|
||||
- text/javascript
|
||||
responses:
|
||||
"200":
|
||||
description: The JavaScript contents
|
||||
schema:
|
||||
type: string
|
||||
summary: Get the dynamic frontend configuration javascript.
|
||||
tags:
|
||||
- Configuration
|
||||
/csrf:
|
||||
get:
|
||||
operationId: base_handleCsrfGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
summary: Get a CSRF token for the current session.
|
||||
tags:
|
||||
- Security
|
||||
/hostname:
|
||||
get:
|
||||
description: Nothing more to describe...
|
||||
operationId: test_handleHostnameGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get the current host name.
|
||||
tags:
|
||||
- Testing
|
||||
/interface/all:
|
||||
get:
|
||||
operationId: interfaces_handleAllGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.Interface'
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get all available interfaces.
|
||||
tags:
|
||||
- Interface
|
||||
/interface/get/{id}:
|
||||
get:
|
||||
operationId: interfaces_handleSingleGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/model.Interface'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get single interface.
|
||||
tags:
|
||||
- Interface
|
||||
/interface/peers/{id}:
|
||||
get:
|
||||
operationId: interfaces_handlePeersGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.Peer'
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get peers for the given interface.
|
||||
tags:
|
||||
- Interface
|
||||
/now:
|
||||
get:
|
||||
description: Nothing more to describe...
|
||||
operationId: test_handleCurrentTimeGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get the current local time.
|
||||
tags:
|
||||
- Testing
|
||||
/peer/all/{id}:
|
||||
get:
|
||||
operationId: peers_handlePeersGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.Peer'
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get peers for the given interface.
|
||||
tags:
|
||||
- Peer
|
||||
/users:
|
||||
get:
|
||||
operationId: users_handleAllGet
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/model.User'
|
||||
type: array
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Get all user records.
|
||||
tags:
|
||||
- Users
|
||||
swagger: "2.0"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user