Start Github V2 branch

This commit is contained in:
Christoph Haas 2023-02-12 23:13:04 +01:00
parent 3c2c7f325b
commit 94f0b26304
764 changed files with 51180 additions and 11271 deletions

View File

@ -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
View File

@ -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

View 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>

View 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
View 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>

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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}

View File

@ -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;
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
.navbar {
padding: 0.5rem 1rem;
}
.navbar-brand > img {
height: 2rem;
width: auto;
}

View File

@ -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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
&nbsp;&nbsp;&nbsp;
<a href="/admin/device/download?dev={{.Device.DeviceName}}" title="Download interface configuration"><i class="fas fa-download"></i></a>
&nbsp;&nbsp;&nbsp;
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +0,0 @@
{{range $flash := $.Alerts}}
<div class="alert alert-{{$flash.Type}}" role="alert">
{{$flash.Message}}
</div>
{{end}}

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}

View File

@ -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
View File

@ -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
View File

@ -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
View File

@ -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=

View 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

View 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)
}
}
}

View 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
View 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
}

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS test;

View File

@ -0,0 +1,3 @@
CREATE TABLE IF NOT EXISTS test (
firstname VARCHAR(16)
);

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@ -0,0 +1 @@
package tpl_files

View 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}}

View 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
View 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
View 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
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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 ""
}

View 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
View 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()
}

View File

@ -0,0 +1,6 @@
package domain
import "errors"
var ErrNotFound = errors.New("record not found")
var ErrNotUnique = errors.New("record not unique")

View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@ -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"`
}

View File

@ -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
View 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.
*/

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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

View 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"
}
}
}
}
}

View 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