summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--.gitlab-ci.yml31
-rw-r--r--Dockerfile14
-rw-r--r--Dockerfile.dev6
-rw-r--r--assets/.keep0
-rw-r--r--assets/application.css40
-rw-r--r--assets/application.js13
-rw-r--r--assets/clipboard.min.js7
-rw-r--r--bin/.keep0
-rw-r--r--docker-compose.override.yml47
-rw-r--r--docker-compose.yml35
-rw-r--r--go.mod16
-rw-r--r--go.sum455
-rw-r--r--gogentoo.go66
-rw-r--r--pkg/app/handler/admin/admin.go21
-rw-r--r--pkg/app/handler/admin/utils.go60
-rw-r--r--pkg/app/handler/auth/handlers.go101
-rw-r--r--pkg/app/handler/auth/init.go47
-rw-r--r--pkg/app/handler/auth/user.go35
-rw-r--r--pkg/app/handler/index/index.go38
-rw-r--r--pkg/app/handler/links/create.go66
-rw-r--r--pkg/app/handler/links/delete.go41
-rw-r--r--pkg/app/handler/links/show.go19
-rw-r--r--pkg/app/handler/links/utils.go157
-rw-r--r--pkg/app/serve.go74
-rw-r--r--pkg/config/config.go75
-rw-r--r--pkg/database/connection.go68
-rw-r--r--pkg/logger/loggers.go39
-rw-r--r--pkg/models/link.go13
-rw-r--r--pkg/models/projects.go25
-rw-r--r--pkg/models/user.go61
-rw-r--r--web/templates/admin/show.tmpl121
-rw-r--r--web/templates/layout/footer.tmpl51
-rw-r--r--web/templates/layout/head.tmpl12
-rw-r--r--web/templates/layout/header.tmpl6
-rw-r--r--web/templates/layout/sitetitle.tmpl37
-rw-r--r--web/templates/layout/tyriannav.tmpl34
-rw-r--r--web/templates/links/create.tmpl81
-rw-r--r--web/templates/links/created.tmpl52
-rw-r--r--web/templates/links/show.tmpl173
40 files changed, 2241 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cd43d2a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+bin
+.idea
+node_modules
+assets
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..d815c1d
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,31 @@
+stages:
+ - build
+
+build:
+ stage: build
+ except:
+ - tags
+ variables:
+ IMAGE_TAG: $CI_REGISTRY_IMAGE/$CI_COMMIT_BRANCH:$CI_COMMIT_SHA
+ LATEST_IMAGE_TAG: $CI_REGISTRY_IMAGE/$CI_COMMIT_BRANCH:latest
+ script:
+ - echo $IMAGE_TAG
+ - echo $LATEST_IMAGE_TAG
+ - docker info
+ - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
+ - docker build --no-cache -t $IMAGE_TAG -t $LATEST_IMAGE_TAG .
+ - docker push $LATEST_IMAGE_TAG
+ - docker push $IMAGE_TAG
+
+build-tag:
+ stage: build
+ only:
+ - tags
+ variables:
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
+ script:
+ - echo $IMAGE_TAG
+ - docker info
+ - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
+ - docker build -t $IMAGE_TAG .
+ - docker push $IMAGE_TAG
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..92f4fed
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,14 @@
+FROM golang:1.14.0 AS builder
+WORKDIR /go/src/go-gentoo
+COPY . /go/src/go-gentoo
+RUN go get github.com/go-pg/pg/v9
+RUN go get github.com/mcuadros/go-version
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o bin .
+
+FROM scratch
+WORKDIR /go/src/go-gentoo
+COPY --from=builder /go/src/go-gentoo/assets /go/src/go-gentoo/assets
+COPY --from=builder /go/src/go-gentoo/bin /go/src/go-gentoo/bin
+COPY --from=builder /go/src/go-gentoo/pkg /go/src/go-gentoo/pkg
+COPY --from=builder /go/src/go-gentoo/web /go/src/go-gentoo/web
+ENTRYPOINT ["/go/src/go-gentoo/bin/go-gentoo", "--serve"]
diff --git a/Dockerfile.dev b/Dockerfile.dev
new file mode 100644
index 0000000..2e346d6
--- /dev/null
+++ b/Dockerfile.dev
@@ -0,0 +1,6 @@
+FROM golang:1.14.0
+RUN apt update && apt install -y ca-certificates ntp ntpdate
+WORKDIR /go/src/go-gentoo
+COPY . /go/src/go-gentoo
+
+CMD tail -f /dev/null
diff --git a/assets/.keep b/assets/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/assets/.keep
diff --git a/assets/application.css b/assets/application.css
new file mode 100644
index 0000000..204e3c8
--- /dev/null
+++ b/assets/application.css
@@ -0,0 +1,40 @@
+.typeahead-field input,
+.typeahead-select {
+ display:block;
+ width:100%;
+ font-size:13px;
+ color:#555;
+ background:0 0;
+ border-radius:2px 0 0 2px;
+ -webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);
+ box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);
+ -webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+ -o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;
+ transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s
+}
+.typeahead-field input {
+ -webkit-appearance:none;
+ background:0 0
+}
+.typeahead-field input:last-child,
+.typeahead-hint {
+ background:#fff
+}
+
+.separator {
+ display: flex;
+ align-items: center;
+ text-align: center;
+ color: grey;
+}
+.separator::before, .separator::after {
+ content: '';
+ flex: 1;
+ border-bottom: 1px solid lightgrey;
+}
+.separator::before {
+ margin-right: .25em;
+}
+.separator::after {
+ margin-left: .25em;
+}
diff --git a/assets/application.js b/assets/application.js
new file mode 100644
index 0000000..7dd3951
--- /dev/null
+++ b/assets/application.js
@@ -0,0 +1,13 @@
+function togglePrefix(){
+ if(document.getElementById('prefix').value == ''){
+ document.getElementById('token').value = "";
+ document.getElementById('token').disabled = true;
+ document.getElementById('token').placeholder = "Only available for prefix != '/'";
+ document.getElementById('index').value = "no";
+ document.getElementById('index').disabled = true;
+ }else{
+ document.getElementById('token').disabled = false;
+ document.getElementById('token').placeholder = "Automatically generated if empty";
+ document.getElementById('index').disabled = false;
+ }
+}
diff --git a/assets/clipboard.min.js b/assets/clipboard.min.js
new file mode 100644
index 0000000..b9ed143
--- /dev/null
+++ b/assets/clipboard.min.js
@@ -0,0 +1,7 @@
+/*!
+ * clipboard.js v2.0.6
+ * https://clipboardjs.com/
+ *
+ * Licensed MIT © Zeno Rocha
+ */
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return o={},r.m=n=[function(t,e){t.exports=function(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){var o=this;function r(){o.off(t,r),e.apply(n,arguments)}return r._=e,this.on(t,r,n)},emit:function(t){for(var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;o<r;o++)n[o].fn.apply(n[o].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),o=n[t],r=[];if(o&&e)for(var i=0,a=o.length;i<a;i++)o[i].fn!==e&&o[i].fn._!==e&&r.push(o[i]);return r.length?n[t]=r:delete n[t],this}},t.exports=n,t.exports.TinyEmitter=n},function(t,e,n){var d=n(3),h=n(4);t.exports=function(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!d.string(e))throw new TypeError("Second argument must be a String");if(!d.fn(n))throw new TypeError("Third argument must be a Function");if(d.node(t))return s=e,f=n,(u=t).addEventListener(s,f),{destroy:function(){u.removeEventListener(s,f)}};if(d.nodeList(t))return a=t,c=e,l=n,Array.prototype.forEach.call(a,function(t){t.addEventListener(c,l)}),{destroy:function(){Array.prototype.forEach.call(a,function(t){t.removeEventListener(c,l)})}};if(d.string(t))return o=t,r=e,i=n,h(document.body,o,r,i);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList");var o,r,i,a,c,l,u,s,f}},function(t,n){n.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},n.nodeList=function(t){var e=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===e||"[object HTMLCollection]"===e)&&"length"in t&&(0===t.length||n.node(t[0]))},n.string=function(t){return"string"==typeof t||t instanceof String},n.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e,n){var a=n(5);function i(t,e,n,o,r){var i=function(e,n,t,o){return function(t){t.delegateTarget=a(t.target,n),t.delegateTarget&&o.call(e,t)}}.apply(this,arguments);return t.addEventListener(n,i,r),{destroy:function(){t.removeEventListener(n,i,r)}}}t.exports=function(t,e,n,o,r){return"function"==typeof t.addEventListener?i.apply(null,arguments):"function"==typeof n?i.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return i(t,e,n,o,r)}))}},function(t,e){if("undefined"!=typeof Element&&!Element.prototype.matches){var n=Element.prototype;n.matches=n.matchesSelector||n.mozMatchesSelector||n.msMatchesSelector||n.oMatchesSelector||n.webkitMatchesSelector}t.exports=function(t,e){for(;t&&9!==t.nodeType;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}},function(t,e,n){"use strict";n.r(e);var o=n(0),r=n.n(o),i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};function a(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function c(t){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,c),this.resolveOptions(t),this.initSelection()}var l=(function(t,e,n){return e&&a(t.prototype,e),n&&a(t,n),t}(c,[{key:"resolveOptions",value:function(t){var e=0<arguments.length&&void 0!==t?t:{};this.action=e.action,this.container=e.container,this.emitter=e.emitter,this.target=e.target,this.text=e.text,this.trigger=e.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=r()(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=r()(this.target),this.copyText()}},{key:"copyText",value:function(){var e=void 0;try{e=document.execCommand(this.action)}catch(t){e=!1}this.handleResult(e)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),document.activeElement.blur(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(t){var e=0<arguments.length&&void 0!==t?t:"copy";if(this._action=e,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":i(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),c),u=n(1),s=n.n(u),f=n(2),d=n.n(f),h="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},p=function(t,e,n){return e&&y(t.prototype,e),n&&y(t,n),t};function y(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}var m=(function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(v,s.a),p(v,[{key:"resolveOptions",value:function(t){var e=0<arguments.length&&void 0!==t?t:{};this.action="function"==typeof e.action?e.action:this.defaultAction,this.target="function"==typeof e.target?e.target:this.defaultTarget,this.text="function"==typeof e.text?e.text:this.defaultText,this.container="object"===h(e.container)?e.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=d()(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return b("action",t)}},{key:"defaultTarget",value:function(t){var e=b("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return b("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(t){var e=0<arguments.length&&void 0!==t?t:["copy","cut"],n="string"==typeof e?[e]:e,o=!!document.queryCommandSupported;return n.forEach(function(t){o=o&&!!document.queryCommandSupported(t)}),o}}]),v);function v(t,e){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,v);var n=function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,(v.__proto__||Object.getPrototypeOf(v)).call(this));return n.resolveOptions(e),n.listenClick(t),n}function b(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}e.default=m}],r.c=o,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=6).default;function r(t){if(o[t])return o[t].exports;var e=o[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}var n,o});
diff --git a/bin/.keep b/bin/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/bin/.keep
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
new file mode 100644
index 0000000..e2779d4
--- /dev/null
+++ b/docker-compose.override.yml
@@ -0,0 +1,47 @@
+version: '3.2'
+
+services:
+ http-serving:
+ build:
+ context: .
+ dockerfile: Dockerfile.dev
+ volumes:
+ - type: "bind"
+ source: "/var/log/go-gentoo"
+ target: "/var/log/go-gentoo"
+ - type: "bind"
+ source: "."
+ target: "/go/src/go-gentoo"
+ environment:
+ GO_GENTOO_LOG_FILE: '/var/log/go-gentoo/web.log'
+ GO_GENTOO_DEVMODE: 'true'
+# GO_GENTOO_DEBUG: 'true'
+ ports:
+ - 127.0.0.1:5000:5000
+ depends_on:
+ - db
+ db:
+ image: postgres:12
+ restart: always
+ environment:
+ POSTGRES_USER: ${GO_GENTOO_POSTGRES_USER:-root}
+ POSTGRES_PASSWORD: ${GO_GENTOO_POSTGRES_PASSWORD:-root}
+ POSTGRES_DB: ${GO_GENTOO_POSTGRES_DB:-gogentoo}
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ pgadmin:
+ image: dpage/pgadmin4
+ logging:
+ driver: none
+ environment:
+ PGADMIN_DEFAULT_EMAIL: admin@admin.org
+ PGADMIN_DEFAULT_PASSWORD: admin
+ volumes:
+ - pgadmin:/root/.pgadmin
+ ports:
+ - "5050:80"
+ restart: unless-stopped
+
+volumes:
+ pgdata:
+ pgadmin:
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..022f6a6
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,35 @@
+version: '3.2'
+
+services:
+ http-serving:
+ image: ${SOKO_IMAGE:-gentoo/go-gentoo:latest}
+ volumes:
+ - type: "bind"
+ source: "/var/log/go-gentoo"
+ target: "/var/log/go-gentoo"
+ ports:
+ - 127.0.0.1:5000:5000
+ labels:
+ com.centurylinklabs.watchtower.enable: "true"
+ restart: always
+ environment:
+ SOKO_LOG_FILE: '/var/log/go-gentoo/web.log'
+ depends_on:
+ - db
+ db:
+ image: postgres:12
+ restart: always
+ environment:
+ POSTGRES_USER: ${SOKO_POSTGRES_USER:-root}
+ POSTGRES_PASSWORD: ${SOKO_POSTGRES_PASSWORD:-root}
+ POSTGRES_DB: ${SOKO_POSTGRES_DB:-gogentoo}
+ shm_size: 512mb
+ volumes:
+ - ${POSTGRES_DATA_PATH:-/var/lib/gentoo-go/data}:/var/lib/postgresql/data
+ watchtower:
+ image: containrrr/watchtower:0.3.10
+ restart: always
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - /root/.docker/config.json:/config.json
+ command: --label-enable
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..3f1a362
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,16 @@
+module go-gentoo
+
+go 1.15
+
+require (
+ github.com/catinello/base62 v0.0.0-20160325105823-e0daaeb631c9
+ github.com/coreos/go-oidc v2.2.1+incompatible
+ github.com/go-pg/pg/v10 v10.7.3 // indirect
+ github.com/go-pg/pg/v9 v9.2.0
+ github.com/google/go-github v17.0.0+incompatible
+ github.com/google/go-querystring v1.0.0 // indirect
+ github.com/gorilla/sessions v1.2.1
+ github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
+ golang.org/x/oauth2 v0.0.0-20201207163604-931764155e3f
+ gopkg.in/square/go-jose.v2 v2.5.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e75a2b9
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,455 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/catinello/base62 v0.0.0-20160325105823-e0daaeb631c9 h1:Wwb8B2cNpapZu1JAunAxwn59RaxUOsIoGY9vNCraF14=
+github.com/catinello/base62 v0.0.0-20160325105823-e0daaeb631c9/go.mod h1:5LcXhHD4xsYgujv9TLgV4rIhOxGqBJgpdUG42MHSKaQ=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/codemodus/kace v0.5.1 h1:4OCsBlE2c/rSJo375ggfnucv9eRzge/U5LrrOZd47HA=
+github.com/codemodus/kace v0.5.1/go.mod h1:coddaHoX1ku1YFSe4Ip0mL9kQjJvKkzb9CfIdG1YR04=
+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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-pg/pg v8.0.7+incompatible h1:ty/sXL1OZLo+47KK9N8llRcmbA9tZasqbQ/OO4ld53g=
+github.com/go-pg/pg/v10 v10.7.3 h1:oL/Hz5MJie/9epmwxlZjfReO+2wlLPOK6BeGb9I+XHk=
+github.com/go-pg/pg/v10 v10.7.3/go.mod h1:UsDYtA+ihbBNX1OeIvDejxkL4RXzL3wsZYoEv5NUEqM=
+github.com/go-pg/pg/v9 v9.2.0 h1:AC+lI8RFFJwf8Pesb7AnUPWv94zhxHZo3MlAKuLE3TE=
+github.com/go-pg/pg/v9 v9.2.0/go.mod h1:fG8qbL+ei4e/fCZLHK+Z+/7b9B+pliZtbpaucG4/YNQ=
+github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
+github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
+github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac h1:jWKYCNlX4J5s8M0nHYkh7Y7c9gRVDEb3mq51j5J0F5M=
+github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/segmentio/encoding v0.1.15 h1:btgfyAuFo3uLw7eOrRDPo8H4Bc881+bSPHzAEe0ukho=
+github.com/segmentio/encoding v0.1.15/go.mod h1:RWhr02uzMB9gQC1x+MfYxedtmBibb9cZ6Vv9VxRSSbw=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+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/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
+github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
+github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
+github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
+github.com/vmihailenco/msgpack/v4 v4.3.11/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
+github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U=
+github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
+github.com/vmihailenco/msgpack/v5 v5.0.0 h1:nCaMMPEyfgwkGc/Y0GreJPhuvzqCqW+Ufq5lY7zLO2c=
+github.com/vmihailenco/msgpack/v5 v5.0.0/go.mod h1:HVxBVPUK/+fZMonk4bi1islLa8V3cfnBug0+4dykPzo=
+github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
+github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opentelemetry.io/otel v0.14.0 h1:YFBEfjCk9MTjaytCNSUkp9Q8lF7QJezA06T71FbQxLQ=
+go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw=
+golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o=
+golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20201203001011-0b49973bad19 h1:ZD+2Sd/BnevwJp8PSli8WgGAGzb9IZtxBsv1iZMYeEA=
+golang.org/x/oauth2 v0.0.0-20201203001011-0b49973bad19/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201207163604-931764155e3f h1:bGuVhRryQ3m1t3U3cQOa4TdSuMIXKrTrvmdJjQLbMKc=
+golang.org/x/oauth2 v0.0.0-20201207163604-931764155e3f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/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 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
+gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
+mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/gogentoo.go b/gogentoo.go
new file mode 100644
index 0000000..bfbcd0b
--- /dev/null
+++ b/gogentoo.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "flag"
+ "go-gentoo/pkg/app"
+ "go-gentoo/pkg/config"
+ "go-gentoo/pkg/logger"
+ "io"
+ "io/ioutil"
+ "os"
+ "time"
+)
+
+func main() {
+
+ //waitForPostgres()
+
+ errorLogFile := logger.CreateLogFile(config.LogFile())
+ defer errorLogFile.Close()
+ initLoggers(os.Stdout, errorLogFile)
+
+ serve := flag.Bool("serve", false, "Start serving the application")
+ help := flag.Bool("help", false, "Print the usage of this application")
+ flag.Parse()
+
+ if *serve {
+ app.Serve()
+ }
+
+ if *help {
+ flag.PrintDefaults()
+ }
+}
+
+// initialize the loggers depending on whether
+// config.debug is set to true
+func initLoggers(infoHandler io.Writer, errorHandler io.Writer) {
+ if config.Debug() == "true" {
+ logger.Init(os.Stdout, infoHandler, errorHandler)
+ } else {
+ logger.Init(ioutil.Discard, infoHandler, errorHandler)
+ }
+}
+
+// TODO this has to be solved differently
+// wait for postgres to come up
+func waitForPostgres() {
+ time.Sleep(5 * time.Second)
+}
+
+// Login
+
+// Logout
+
+// Create New
+// - [optional] select project
+// - [optional] select expire
+// - [optional] select path
+
+// View existing
+// - personal
+// - per Project
+// => options per entry
+// - change expire
+// - delete
+// (- add new)
diff --git a/pkg/app/handler/admin/admin.go b/pkg/app/handler/admin/admin.go
new file mode 100644
index 0000000..299f0b7
--- /dev/null
+++ b/pkg/app/handler/admin/admin.go
@@ -0,0 +1,21 @@
+// Used to show the landing page of the application
+
+package admin
+
+import (
+ "html/template"
+ "net/http"
+)
+
+// Show renders a template to show the landing page of the application
+func Show(w http.ResponseWriter, r *http.Request) {
+
+ templates := template.Must(
+ template.Must(
+ template.New("Show").
+ Funcs(getFuncMap()).
+ ParseGlob("web/templates/layout/*.tmpl")).
+ ParseGlob("web/templates/admin/*.tmpl"))
+
+ templates.ExecuteTemplate(w, "show.tmpl", getPageData(w, r))
+}
diff --git a/pkg/app/handler/admin/utils.go b/pkg/app/handler/admin/utils.go
new file mode 100644
index 0000000..67bb0d6
--- /dev/null
+++ b/pkg/app/handler/admin/utils.go
@@ -0,0 +1,60 @@
+// miscellaneous utility functions used for the landing page of the application
+
+package admin
+
+import (
+ "crypto/md5"
+ "fmt"
+ "go-gentoo/pkg/app/handler/auth"
+ "go-gentoo/pkg/database"
+ "go-gentoo/pkg/models"
+ "html/template"
+ "net/http"
+ "strings"
+)
+
+func getPageData(w http.ResponseWriter, r *http.Request) interface{} {
+ user := auth.GetUser(w, r)
+ return struct {
+ Tab string
+ User *models.User
+ UserLinks []models.Link
+ }{
+ Tab: "admin",
+ User: user,
+ UserLinks: getAllLinks(),
+ }
+}
+
+func getFuncMap() template.FuncMap {
+ return template.FuncMap{
+ "gravatar": emailToGravater,
+ "replaceAll": strings.ReplaceAll,
+ "getPrefixList": getPrefixList,
+ }
+}
+
+func emailToGravater(email string) string {
+ return "https://www.gravatar.com/avatar/" + fmt.Sprintf("%x", md5.Sum([]byte(email)))
+}
+
+func getAllLinks() []models.Link {
+ var links []models.Link
+ database.DBCon.Model(&links).
+ Select()
+ return links
+}
+
+func getPrefixList(links []models.Link) []string {
+ prefixMap := make(map[string]bool)
+ var prefixList []string
+ for _, link := range links {
+ if link.Prefix != "" {
+ prefixMap[link.Prefix] = true
+ }
+ }
+ for key, _ := range prefixMap {
+ prefixList = append(prefixList, key)
+ }
+ return prefixList
+}
diff --git a/pkg/app/handler/auth/handlers.go b/pkg/app/handler/auth/handlers.go
new file mode 100644
index 0000000..8e79959
--- /dev/null
+++ b/pkg/app/handler/auth/handlers.go
@@ -0,0 +1,101 @@
+package auth
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "go-gentoo/pkg/config"
+ "go-gentoo/pkg/models"
+ "golang.org/x/oauth2"
+ "net/http"
+ "net/url"
+)
+
+func Login(w http.ResponseWriter, r *http.Request) {
+ b := make([]byte, 16)
+ rand.Read(b)
+
+ state := base64.URLEncoding.EncodeToString(b)
+
+ session, _ := CookieStore.Get(r, config.SessionStoreKey())
+ session.Values["state"] = state
+ session.Save(r, w)
+
+ url := Oauth2Config.AuthCodeURL(state)
+ http.Redirect(w, r, url, http.StatusFound)
+}
+
+func Callback(w http.ResponseWriter, r *http.Request) {
+ session, err := CookieStore.Get(r, config.SessionStoreKey())
+
+ if err != nil {
+ http.Error(w, "state did not match", http.StatusBadRequest)
+ return
+ }
+
+ if r.URL.Query().Get("state") != session.Values["state"] {
+ http.Error(w, "state did not match", http.StatusBadRequest)
+ return
+ }
+
+ oauth2Token, err := Oauth2Config.Exchange(Ctx, r.URL.Query().Get("code"))
+ if err != nil {
+ http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ rawIDToken, ok := oauth2Token.Extra("id_token").(string)
+ if !ok {
+ http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
+ return
+ }
+ idToken, err := Verifier.Verify(Ctx, rawIDToken)
+ if err != nil {
+ http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ resp := struct {
+ OAuth2Token *oauth2.Token
+ IDTokenClaims *json.RawMessage // ID Token payload is just JSON.
+ }{oauth2Token, new(json.RawMessage)}
+
+ if err := idToken.Claims(&resp.IDTokenClaims); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ type Response struct {
+ GivenName string `json:"given_name"`
+ Username string `json:"preferred_username"`
+ Email string `json:"email"`
+ }
+
+ var keycloakResponse Response
+ err = json.Unmarshal(*resp.IDTokenClaims, &keycloakResponse)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ session.Values["idToken"] = rawIDToken
+ session.Values["user"] = models.User{
+ Email: keycloakResponse.Email,
+ RealName: keycloakResponse.GivenName,
+ UserName: keycloakResponse.Username,
+ Projects: nil,
+ }
+ err = session.Save(r, w)
+
+ http.Redirect(w, r, "/", http.StatusFound)
+}
+
+// http://www.gorillatoolkit.org/pkg/sessions#CookieStore.MaxAge
+func Logout(w http.ResponseWriter, r *http.Request) {
+ session, err := CookieStore.Get(r, config.SessionStoreKey())
+ if err != nil {
+ return
+ }
+ session.Options.MaxAge = -1
+ session.Save(r, w)
+ http.Redirect(w, r, config.OIDConfigURL()+"/protocol/openid-connect/logout?redirect_uri="+url.QueryEscape(config.ApplicationURL()), 302)
+}
diff --git a/pkg/app/handler/auth/init.go b/pkg/app/handler/auth/init.go
new file mode 100644
index 0000000..e97e997
--- /dev/null
+++ b/pkg/app/handler/auth/init.go
@@ -0,0 +1,47 @@
+package auth
+
+import (
+ "context"
+ "encoding/gob"
+ "github.com/coreos/go-oidc"
+ "github.com/gorilla/sessions"
+ "go-gentoo/pkg/config"
+ "go-gentoo/pkg/models"
+ "golang.org/x/oauth2"
+)
+
+var (
+ Oauth2Config oauth2.Config
+ Verifier *oidc.IDTokenVerifier
+ Ctx context.Context
+ CookieStore *sessions.CookieStore
+)
+
+func Init() {
+ gob.Register(&models.User{})
+
+ Ctx = context.Background()
+ provider, err := oidc.NewProvider(Ctx, config.OIDConfigURL())
+ if err != nil {
+ panic(err)
+ }
+
+ CookieStore = sessions.NewCookieStore([]byte(config.SessionSecret()))
+
+ // Configure an OpenID Connect aware OAuth2 client.
+ Oauth2Config = oauth2.Config{
+ ClientID: config.OIDClientID(),
+ ClientSecret: config.OIDClientSecret(),
+ RedirectURL: config.ApplicationURL() + "/auth/callback",
+ // Discovery returns the OAuth2 endpoints.
+ Endpoint: provider.Endpoint(),
+ // "openid" is a required scope for OpenID Connect flows.
+ Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
+ }
+
+ oidcConfig := &oidc.Config{
+ ClientID: config.OIDClientID(),
+ }
+ Verifier = provider.Verifier(oidcConfig)
+
+}
diff --git a/pkg/app/handler/auth/user.go b/pkg/app/handler/auth/user.go
new file mode 100644
index 0000000..9761d80
--- /dev/null
+++ b/pkg/app/handler/auth/user.go
@@ -0,0 +1,35 @@
+package auth
+
+import (
+ "go-gentoo/pkg/config"
+ "go-gentoo/pkg/models"
+ "net/http"
+)
+
+func IsValidUser(w http.ResponseWriter, r *http.Request) bool {
+ session, err := CookieStore.Get(r, config.SessionStoreKey())
+
+ if err != nil {
+ return false
+ }
+
+ if token, ok := session.Values["idToken"].(string); ok {
+ _, err = Verifier.Verify(Ctx, token)
+ return err == nil
+ }
+
+ return false
+}
+
+func GetUser(w http.ResponseWriter, r *http.Request) *models.User {
+ session, err := CookieStore.Get(r, config.SessionStoreKey())
+ if err != nil {
+ return nil
+ }
+ user := session.Values["user"].(*models.User)
+ err = user.ComputeProjects()
+ if err != nil {
+ return nil
+ }
+ return user
+}
diff --git a/pkg/app/handler/index/index.go b/pkg/app/handler/index/index.go
new file mode 100644
index 0000000..e31d32c
--- /dev/null
+++ b/pkg/app/handler/index/index.go
@@ -0,0 +1,38 @@
+// Used to show the landing page of the application
+
+package index
+
+import (
+ "go-gentoo/pkg/database"
+ "go-gentoo/pkg/models"
+ "net/http"
+)
+
+// Show renders a template to show the landing page of the application
+func Handle(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ redirect(w, r)
+ return
+ }
+ http.Redirect(w, r, "/links/create", http.StatusFound)
+}
+
+func redirect(w http.ResponseWriter, r *http.Request) {
+ links := getLink(r.URL.Path)
+ if len(links) != 1 {
+ http.Error(w, "Not found", http.StatusNotFound)
+ } else {
+ link := links[0]
+ link.Hits++
+ database.DBCon.Model(&link).WherePK().Update()
+ http.Redirect(w, r, links[0].TargetLink, http.StatusFound)
+ }
+}
+
+func getLink(shortlink string) []models.Link {
+ var links []models.Link
+ database.DBCon.Model(&links).
+ Where("short_link = ?", shortlink).
+ Select()
+ return links
+}
diff --git a/pkg/app/handler/links/create.go b/pkg/app/handler/links/create.go
new file mode 100644
index 0000000..3d88f59
--- /dev/null
+++ b/pkg/app/handler/links/create.go
@@ -0,0 +1,66 @@
+package links
+
+import (
+ "go-gentoo/pkg/app/handler/auth"
+ "html/template"
+ "net/http"
+)
+
+// Show renders a template to show the landing page of the application
+func Create(w http.ResponseWriter, r *http.Request) {
+ switch r.Method {
+ case http.MethodGet:
+ showForm(w, r)
+ case http.MethodPost:
+ createLink(w, r)
+ }
+}
+
+func showForm(w http.ResponseWriter, r *http.Request) {
+
+ templates := template.Must(
+ template.Must(
+ template.New("Show").
+ Funcs(getFuncMap()).
+ ParseGlob("web/templates/layout/*.tmpl")).
+ ParseGlob("web/templates/links/create.tmpl"))
+
+ templates.ExecuteTemplate(w, "create.tmpl", getPageData(w, r, "create"))
+}
+
+func createLink(w http.ResponseWriter, r *http.Request) {
+ user := auth.GetUser(w, r)
+
+ r.ParseForm()
+ prefix := r.Form.Get("prefix")
+ index := r.Form.Get("index")
+ token := r.Form.Get("token")
+ target := r.Form.Get("target")
+
+ if !user.IsAdmin() && prefix == "" {
+ token = ""
+ }
+
+ if isValidPrefix(user, prefix) && isValidToken(prefix) && isValidToken(token) && isValidTarget(target) {
+ shortlink, ok := createShortURL(prefix, token, target, user.Email, index == "yes", 0)
+ if ok {
+ renderCreatedTemplate(w, r, shortlink, target)
+ return
+ } else {
+ http.Error(w, "Could not create shortened URL", http.StatusInternalServerError)
+ }
+ } else {
+ http.Error(w, "Params are not valid", http.StatusBadRequest)
+ }
+}
+
+func renderCreatedTemplate(w http.ResponseWriter, r *http.Request, shortlink, target string) {
+ templates := template.Must(
+ template.Must(
+ template.New("Show").
+ Funcs(getFuncMap()).
+ ParseGlob("web/templates/layout/*.tmpl")).
+ ParseGlob("web/templates/links/created.tmpl"))
+
+ templates.ExecuteTemplate(w, "created.tmpl", getCreatePageData(w, r, shortlink, target))
+}
diff --git a/pkg/app/handler/links/delete.go b/pkg/app/handler/links/delete.go
new file mode 100644
index 0000000..ee4bce4
--- /dev/null
+++ b/pkg/app/handler/links/delete.go
@@ -0,0 +1,41 @@
+package links
+
+import (
+ "go-gentoo/pkg/app/handler/auth"
+ "go-gentoo/pkg/database"
+ "go-gentoo/pkg/models"
+ "net/http"
+)
+
+func Delete(w http.ResponseWriter, r *http.Request) {
+ user := auth.GetUser(w, r)
+
+ r.ParseForm()
+ prefix := r.Form.Get("prefix")
+ token := r.Form.Get("token")
+ from := r.Form.Get("from")
+ var shortlink string
+ if prefix != "" {
+ shortlink = "/" + prefix + "/" + token
+ } else {
+ shortlink = "/" + token
+ }
+
+ links := getLink(shortlink)
+
+ if len(links) != 1 {
+ http.Error(w, "Could not delete shortened URL", http.StatusInternalServerError)
+ return
+ }
+
+ if user.IsAdmin() || links[0].UserEmail == user.Email || contains(user.Projects, links[0].Prefix) || links[0].Prefix == user.UserName {
+ link := new(models.Link)
+ _, err := database.DBCon.Model(link).Where("short_link = ?", shortlink).Delete()
+ if err != nil {
+ http.Error(w, "Could not delete shortened URL", http.StatusInternalServerError)
+ } else {
+ http.Redirect(w, r, from, http.StatusFound)
+ }
+ }
+
+}
diff --git a/pkg/app/handler/links/show.go b/pkg/app/handler/links/show.go
new file mode 100644
index 0000000..4e5c5c6
--- /dev/null
+++ b/pkg/app/handler/links/show.go
@@ -0,0 +1,19 @@
+package links
+
+import (
+ "html/template"
+ "net/http"
+)
+
+// Show renders a template to show the landing page of the application
+func Show(w http.ResponseWriter, r *http.Request) {
+
+ templates := template.Must(
+ template.Must(
+ template.New("Show").
+ Funcs(getFuncMap()).
+ ParseGlob("web/templates/layout/*.tmpl")).
+ ParseGlob("web/templates/links/show.tmpl"))
+
+ templates.ExecuteTemplate(w, "show.tmpl", getPageData(w, r, "show"))
+}
diff --git a/pkg/app/handler/links/utils.go b/pkg/app/handler/links/utils.go
new file mode 100644
index 0000000..26afbbf
--- /dev/null
+++ b/pkg/app/handler/links/utils.go
@@ -0,0 +1,157 @@
+package links
+
+import (
+ "crypto/md5"
+ "fmt"
+ "github.com/catinello/base62"
+ "go-gentoo/pkg/app/handler/auth"
+ "go-gentoo/pkg/config"
+ "go-gentoo/pkg/database"
+ "go-gentoo/pkg/models"
+ "html/template"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+const (
+ MAX_TRY = 10
+ MAX_TOKEN_LENGTH = 75
+)
+
+func getPageData(w http.ResponseWriter, r *http.Request, tab string) interface{} {
+ user := auth.GetUser(w, r)
+ return struct {
+ Tab string
+ User *models.User
+ UserLinks []models.Link
+ }{
+ Tab: tab,
+ User: user,
+ UserLinks: getLinks(user),
+ }
+}
+
+func getCreatePageData(w http.ResponseWriter, r *http.Request, shortlink, target string) interface{} {
+ return struct {
+ Tab string
+ User *models.User
+ ShortLink string
+ Target string
+ }{
+ Tab: "new",
+ User: auth.GetUser(w, r),
+ ShortLink: config.ApplicationURL() + shortlink,
+ Target: target,
+ }
+}
+
+func getFuncMap() template.FuncMap {
+ return template.FuncMap{
+ "gravatar": emailToGravater,
+ "replaceAll": strings.ReplaceAll,
+ }
+}
+
+func emailToGravater(email string) string {
+ return "https://www.gravatar.com/avatar/" + fmt.Sprintf("%x", md5.Sum([]byte(email)))
+}
+
+func getLinks(user *models.User) []models.Link {
+ var links []models.Link
+ database.DBCon.Model(&links).
+ Where("user_email = '" + user.Email + "'").
+ WhereOr(createQuery(user)).
+ Select()
+ return links
+}
+
+func createQuery(user *models.User) string {
+ var queryParts []string
+ for _, project := range user.Projects {
+ queryParts = append(queryParts, "prefix = '"+project+"'")
+ }
+ return strings.Join(queryParts, " OR ")
+}
+
+func isValidPrefix(user *models.User, prefix string) bool {
+ return prefixIsNotReserved(prefix) && (contains(user.Projects, prefix) || prefix == "" || prefix == user.UserName)
+}
+
+func prefixIsNotReserved(prefix string) bool {
+ return prefix != "auth" && prefix != "assets" && prefix != "links" && prefix != "admin"
+}
+
+func isValidToken(token string) bool {
+ for _, char := range token {
+ if !strings.Contains(config.ValidURLTokenChars(), strings.ToLower(string(char))) {
+ return false
+ }
+ }
+ return len(token) < MAX_TOKEN_LENGTH
+}
+
+func isValidTarget(target string) bool {
+ _, err := url.ParseRequestURI(target)
+ return err == nil
+}
+
+func getLatestId() int {
+
+ var links []models.Link
+ database.DBCon.Model(&links).
+ Order("id DESC").
+ Limit(1).
+ Select()
+
+ if len(links) != 1 {
+ return 4000
+ }
+
+ return links[0].Id
+}
+
+func createShortURL(prefix, token, target, username string, setIndex bool, try int) (string, bool) {
+ var shortlink string
+ id := getLatestId() + 1
+ if token == "" && (!setIndex || prefix == "") {
+ token = base62.Encode(id)
+ }
+ if prefix != "" {
+ shortlink = "/" + prefix + "/" + token
+ } else {
+ shortlink = "/" + token
+ }
+
+ if len(getLink(shortlink)) > 0 && try <= MAX_TRY {
+ return createShortURL(prefix, token, target, username, setIndex, try+1)
+ }
+
+ err := database.DBCon.Insert(&models.Link{
+ Id: id,
+ Prefix: prefix,
+ URLToken: token,
+ ShortLink: shortlink,
+ TargetLink: target,
+ UserEmail: username,
+ Hits: 0,
+ })
+ return shortlink, err == nil
+}
+
+func getLink(shortlink string) []models.Link {
+ var links []models.Link
+ database.DBCon.Model(&links).
+ Where("short_link = ?", shortlink).
+ Select()
+ return links
+}
+
+func contains(list []string, value string) bool {
+ for _, v := range list {
+ if v == value {
+ return true
+ }
+ }
+ return false
+}
diff --git a/pkg/app/serve.go b/pkg/app/serve.go
new file mode 100644
index 0000000..fc2cdc3
--- /dev/null
+++ b/pkg/app/serve.go
@@ -0,0 +1,74 @@
+package app
+
+import (
+ "go-gentoo/pkg/app/handler/admin"
+ "go-gentoo/pkg/app/handler/auth"
+ "go-gentoo/pkg/app/handler/index"
+ "go-gentoo/pkg/app/handler/links"
+ "go-gentoo/pkg/config"
+ "go-gentoo/pkg/database"
+ "go-gentoo/pkg/logger"
+ "log"
+ "net/http"
+)
+
+// Serve is used to serve the web application
+func Serve() {
+
+ database.Connect()
+ defer database.DBCon.Close()
+
+ auth.Init()
+ setRoute("/auth/login", auth.Login)
+ setRoute("/auth/logout", auth.Logout)
+ setRoute("/auth/callback", auth.Callback)
+
+ setProtectedRoute("/links/show", links.Show)
+ setProtectedRoute("/links/create", links.Create)
+ setProtectedRoute("/links/delete", links.Delete)
+
+ setProtectedRoute("/admin/", admin.Show)
+
+ setProtectedRoute("/", index.Handle)
+
+ fs := http.StripPrefix("/assets/", http.FileServer(http.Dir("/go/src/go-gentoo/assets")))
+ http.Handle("/assets/", fs)
+
+ logger.Info.Println("Serving on port: " + config.Port())
+ log.Fatal(http.ListenAndServe(":"+config.Port(), nil))
+}
+
+// define a route using the default middleware and the given handler
+func setProtectedRoute(path string, handler http.HandlerFunc) {
+ http.HandleFunc(path, protectedMW(handler))
+}
+
+// mw is used as default middleware to set the default headers
+func protectedMW(handler http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if auth.IsValidUser(w, r) {
+ setDefaultHeaders(w)
+ handler(w, r)
+ } else {
+ http.Redirect(w, r, "/auth/login", 301)
+ }
+ }
+}
+
+// define a route using the default middleware and the given handler
+func setRoute(path string, handler http.HandlerFunc) {
+ http.HandleFunc(path, mw(handler))
+}
+
+// mw is used as default middleware to set the default headers
+func mw(handler http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ setDefaultHeaders(w)
+ handler(w, r)
+ }
+}
+
+// setDefaultHeaders sets the default headers that apply for all pages
+func setDefaultHeaders(w http.ResponseWriter) {
+ w.Header().Set("Cache-Control", "no-cache")
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
new file mode 100644
index 0000000..261ff3a
--- /dev/null
+++ b/pkg/config/config.go
@@ -0,0 +1,75 @@
+package config
+
+import "os"
+
+func PostgresUser() string {
+ return getEnv("GO_GENTOO_POSTGRES_USER", "root")
+}
+
+func PostgresPass() string {
+ return getEnv("GO_GENTOO_POSTGRES_PASS", "root")
+}
+
+func PostgresDb() string {
+ return getEnv("GO_GENTOO_POSTGRES_DB", "gogentoo")
+}
+
+func PostgresHost() string {
+ return getEnv("GO_GENTOO_POSTGRES_HOST", "db")
+}
+
+func PostgresPort() string {
+ return getEnv("GO_GENTOO_POSTGRES_PORT", "5432")
+}
+
+func Port() string {
+ return getEnv("GO_GENTOO_PORT", "5000")
+}
+
+func CacheControl() string {
+ return getEnv("GO_GENTOO_CACHE_CONTROL", "max-age=300")
+}
+
+func Debug() string {
+ return getEnv("GO_GENTOO_DEBUG", "false")
+}
+
+func LogFile() string {
+ return getEnv("GO_GENTOO_LOG_FILE", "/var/log/go-gentoo/errors.log")
+}
+
+func OIDConfigURL() string {
+ return getEnv("GO_GENTOO_OID_CONFIG_URL", "https://sso.gentoo.org/auth/realms/gentoo")
+}
+
+func OIDClientID() string {
+ return getEnv("GO_GENTOO_OID_CLIENT_ID", "demo-client")
+}
+
+func OIDClientSecret() string {
+ return getEnv("GO_GENTOO_OID_CLIENT_SECRET", "00000000-0000-0000-0000-000000000000")
+}
+
+func ApplicationURL() string {
+ return getEnv("GO_GENTOO_APPLICATION_URL", "https://go.gentoo.org")
+}
+
+func SessionStoreKey() string {
+ return getEnv("GO_GENTOO_SESSION_STORE_KEY", "gentoo_sess")
+}
+
+func SessionSecret() string {
+ return getEnv("GO_GENTOO_SESSION_SECRET", "123456789")
+}
+
+func ValidURLTokenChars() string {
+ return getEnv("GO_GENTOO_VALID_URL_TOKEN_CHARS", "abcdefghijklmnopqrstuvwxyz1234567890-")
+}
+
+func getEnv(key string, fallback string) string {
+ if os.Getenv(key) != "" {
+ return os.Getenv(key)
+ } else {
+ return fallback
+ }
+}
diff --git a/pkg/database/connection.go b/pkg/database/connection.go
new file mode 100644
index 0000000..c5e7c7b
--- /dev/null
+++ b/pkg/database/connection.go
@@ -0,0 +1,68 @@
+// Contains utility functions around the database
+
+package database
+
+import (
+ "context"
+ "github.com/go-pg/pg/v9"
+ "github.com/go-pg/pg/v9/orm"
+ "go-gentoo/pkg/config"
+ "go-gentoo/pkg/logger"
+ "go-gentoo/pkg/models"
+ "log"
+)
+
+// DBCon is the connection handle
+// for the database
+var (
+ DBCon *pg.DB
+)
+
+// CreateSchema creates the tables in the database
+// in case they don't alreay exist
+func CreateSchema() error {
+ for _, model := range []interface{}{(*models.Link)(nil)} {
+
+ err := DBCon.CreateTable(model, &orm.CreateTableOptions{
+ IfNotExists: true,
+ })
+ if err != nil {
+ return err
+ }
+
+ }
+ return nil
+}
+
+type dbLogger struct{}
+
+func (d dbLogger) BeforeQuery(c context.Context, q *pg.QueryEvent) (context.Context, error) {
+ return c, nil
+}
+
+// AfterQuery is used to log SQL queries
+func (d dbLogger) AfterQuery(c context.Context, q *pg.QueryEvent) error {
+ logger.Debug.Println(q.FormattedQuery())
+ return nil
+}
+
+// Connect is used to connect to the database
+// and turn on logging if desired
+func Connect() {
+ DBCon = pg.Connect(&pg.Options{
+ User: config.PostgresUser(),
+ Password: config.PostgresPass(),
+ Database: config.PostgresDb(),
+ Addr: config.PostgresHost() + ":" + config.PostgresPort(),
+ })
+
+ DBCon.AddQueryHook(dbLogger{})
+
+ err := CreateSchema()
+ if err != nil {
+ logger.Error.Println("ERROR: Could not create database schema")
+ logger.Error.Println(err)
+ log.Fatalln(err)
+ }
+
+}
diff --git a/pkg/logger/loggers.go b/pkg/logger/loggers.go
new file mode 100644
index 0000000..0ef51e9
--- /dev/null
+++ b/pkg/logger/loggers.go
@@ -0,0 +1,39 @@
+package logger
+
+import (
+ "io"
+ "log"
+ "os"
+)
+
+var (
+ Debug *log.Logger
+ Info *log.Logger
+ Error *log.Logger
+)
+
+func Init(
+ debugHandle io.Writer,
+ infoHandle io.Writer,
+ errorHandle io.Writer) {
+
+ Debug = log.New(debugHandle,
+ "DEBUG: ",
+ log.Ldate|log.Ltime|log.Lshortfile)
+
+ Info = log.New(infoHandle,
+ "INFO: ",
+ log.Ldate|log.Ltime|log.Lshortfile)
+
+ Error = log.New(errorHandle,
+ "ERROR: ",
+ log.Ldate|log.Ltime|log.Lshortfile)
+}
+
+func CreateLogFile(path string) *os.File {
+ f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ log.Println(err)
+ }
+ return f
+}
diff --git a/pkg/models/link.go b/pkg/models/link.go
new file mode 100644
index 0000000..34d96fa
--- /dev/null
+++ b/pkg/models/link.go
@@ -0,0 +1,13 @@
+// Contains the model of the application data
+
+package models
+
+type Link struct {
+ Id int `pg:",pk"`
+ Prefix string
+ URLToken string
+ ShortLink string `pg:",unique"`
+ TargetLink string
+ UserEmail string
+ Hits int
+}
diff --git a/pkg/models/projects.go b/pkg/models/projects.go
new file mode 100644
index 0000000..63b1c35
--- /dev/null
+++ b/pkg/models/projects.go
@@ -0,0 +1,25 @@
+package models
+
+import "encoding/xml"
+
+type ProjectList struct {
+ XMLName xml.Name `xml:"projects"`
+ Projects []Project `xml:"project"`
+}
+
+type Project struct {
+ XMLName xml.Name `xml:"project" pg:"-"`
+ Email string `xml:"email" pg:",pk"`
+ Name string `xml:"name"`
+ Url string `xml:"url"`
+ Description string `xml:"description"`
+ Members []Member `xml:"member"`
+}
+
+type Member struct {
+ XMLName xml.Name `xml:"member" json:"-" pg:"-"`
+ IsLead bool `xml:"is-lead,attr"`
+ Email string `xml:"email"`
+ Name string `xml:"name"`
+ Role string `xml:"role"`
+}
diff --git a/pkg/models/user.go b/pkg/models/user.go
new file mode 100644
index 0000000..09dc247
--- /dev/null
+++ b/pkg/models/user.go
@@ -0,0 +1,61 @@
+// Contains the model of the application data
+
+package models
+
+import (
+ "encoding/xml"
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+type User struct {
+ Email string `pg:",pk"`
+ RealName string
+ UserName string
+ Projects []string
+}
+
+func (u *User) IsAdmin() bool {
+ for _, project := range u.Projects {
+ if project == "infra" {
+ return true
+ }
+ }
+ return false
+}
+
+func (u *User) ComputeProjects() error {
+ projects, err := parseProjectList()
+
+ if err != nil {
+ return err
+ }
+
+ for _, project := range projects.Projects {
+ for _, member := range project.Members {
+ if member.Email == u.Email {
+ abbreviation := strings.ReplaceAll(project.Email, "@gentoo.org", "")
+ u.Projects = append(u.Projects, abbreviation)
+ }
+ }
+ }
+
+ return nil
+}
+
+// parseQAReport gets the xml from qa-reports.gentoo.org and parses it
+func parseProjectList() (ProjectList, error) {
+ resp, err := http.Get("https://api.gentoo.org/metastructure/projects.xml")
+ if err != nil {
+ return ProjectList{}, err
+ }
+ defer resp.Body.Close()
+ xmlData, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return ProjectList{}, err
+ }
+ var projectList ProjectList
+ xml.Unmarshal(xmlData, &projectList)
+ return projectList, err
+}
diff --git a/web/templates/admin/show.tmpl b/web/templates/admin/show.tmpl
new file mode 100644
index 0000000..2dbbb78
--- /dev/null
+++ b/web/templates/admin/show.tmpl
@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<html lang="en">
+{{template "head" .}}
+<body>
+{{template "header" .}}
+
+<div class="container mb-5">
+ <div class="row">
+ <div class="col-12">
+
+ <h2 class="mt-4"><span style="font-family: monospace;">/</span></h2>
+
+ {{$empty := true}}
+
+ <table class="table">
+ <colgroup>
+ <col span="1" style="width: 20%;">
+ <col span="1" style="width: 60%;">
+ <col span="1" style="width: 10%;">
+ <col span="1" style="width: 5%;">
+ <col span="1" style="width: 5%;">
+ </colgroup>
+ <thead>
+ <tr>
+ <th scope="col">Short URL</th>
+ <th scope="col">Target</th>
+ <th scope="col">Creator</th>
+ <th scope="col">Hits</th>
+ <th scope="col">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{$empty = true}}
+ {{range .UserLinks}}
+ {{if eq .Prefix ""}}
+ {{$empty = false}}
+ <tr>
+ <th scope="row"><a class="text-dark" href="{{.ShortLink}}">go.gentoo.org{{.ShortLink}}</a></th>
+ <td><a class="text-dark" href="{{.TargetLink}}">{{.TargetLink}}</a></td>
+ <td>{{replaceAll .UserEmail "@gentoo.org" ""}}</td>
+ <td>{{.Hits}}</td>
+ <td>
+ <form action="/links/delete" method="post">
+ <input hidden name="prefix" value="{{.Prefix}}">
+ <input hidden name="token" value="{{.URLToken}}">
+ <input hidden name="from" value="/admin/">
+ <button type="submit" class="btn btn-link py-0 text-dark">
+ <i class="fa fa-trash-o" aria-hidden="true"></i>
+ </button>
+ </form>
+ </td>
+ </tr>
+ {{end}}
+ {{end}}
+ {{if $empty}}
+ <tr>
+ <td colspan="4" class="text-center"><i>No shortened URLs yet</i></td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+
+
+ {{range $index, $project := getPrefixList .UserLinks}}
+ <h2 class="mt-5"><span style="font-family: monospace;">/{{$project}}/</span></h2>
+
+ <table class="table">
+ <colgroup>
+ <col span="1" style="width: 20%;">
+ <col span="1" style="width: 65%;">
+ <col span="1" style="width: 10%;">
+ <col span="1" style="width: 5%;">
+ </colgroup>
+ <thead>
+ <tr>
+ <th scope="col">Short URL</th>
+ <th scope="col">Target</th>
+ <th scope="col">Creator</th>
+ <th scope="col">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{$empty = true}}
+ {{range $.UserLinks}}
+ {{if eq .Prefix $project}}
+ {{$empty = false}}
+ <tr>
+ <th scope="row"><a class="text-dark" href="{{.ShortLink}}">go.gentoo.org{{.ShortLink}}</a></th>
+ <td><a class="text-dark" href="{{.TargetLink}}">{{.TargetLink}}</a></td>
+ <td>{{replaceAll .UserEmail "@gentoo.org" ""}}</td>
+ <td>
+ <form action="/links/delete" method="post">
+ <input hidden name="prefix" value="{{.Prefix}}">
+ <input hidden name="token" value="{{.URLToken}}">
+ <input hidden name="from" value="/admin/">
+ <button type="submit" class="btn btn-link py-0 text-dark">
+ <i class="fa fa-trash-o" aria-hidden="true"></i>
+ </button>
+ </form>
+ </td>
+ </tr>
+ {{end}}
+ {{end}}
+ {{if $empty}}
+ <tr>
+ <td colspan="4" class="text-center"><i>No shortened URLs yet</i></td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ {{end}}
+ </div>
+ </div>
+</div>
+
+
+{{template "footer" .}}
+
+
+</body>
+</html>
diff --git a/web/templates/layout/footer.tmpl b/web/templates/layout/footer.tmpl
new file mode 100644
index 0000000..3b5efcf
--- /dev/null
+++ b/web/templates/layout/footer.tmpl
@@ -0,0 +1,51 @@
+{{define "footer"}}
+ <footer style="background-color: #fafafa; box-shadow:none!important;">
+ <div class="container pt-4" style="border-top: 1px solid #dddddd;">
+ <div class="row d-none">
+ <div class="col-12 offset-md-2 col-md-7">
+ <h3 class="footerhead">Gentoo Packages Database</h3>
+ <div class="row">
+ <div class="col-xs-12 col-md-4">
+ </div>
+ <div class="col-xs-12 col-md-4">
+ </div>
+ <div class="col-xs-12 col-md-4">
+ </div>
+ </div>
+ </div>
+ <div class="col-12 col-md-3">
+ <h3 class="footerhead">Questions or comments?</h3>
+ Please feel free to <a href="https://www.gentoo.org/inside-gentoo/contact/">contact us</a>.
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-2 col-sm-2 col-md-2">
+ <ul class="footerlinks three-icons">
+ <li><a href="https://twitter.com/gentoo" title="@Gentoo on Twitter"><span class="fa fa-twitter fa-fw"></span></a></li>
+ <li><a href="https://www.facebook.com/gentoo.org" title="Gentoo on Facebook"><span class="fa fa-facebook fa-fw"></span></a></li>
+ <li><a href="https://www.reddit.com/r/Gentoo/" title="Gentoo on Reddit"><span class="fa fa-reddit-alien fa-fw"></span></a></li>
+ </ul>
+ </div>
+ <div class="col-8 col-sm-8 col-md-8">
+ <strong>&copy; 2001&ndash;2020 Gentoo Foundation, Inc.</strong><br>
+ <small>
+ Gentoo is a trademark of the Gentoo Foundation, Inc.
+ The contents of this document, unless otherwise expressly stated, are licensed under the
+ <a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="license">CC-BY-SA-4.0</a> license.
+ The <a href="https://www.gentoo.org/inside-gentoo/foundation/name-logo-guidelines.html">Gentoo Name and Logo Usage Guidelines</a> apply.
+ </small>
+ </div>
+ <div class="col-2 col-sm-2 col-md-2 text-right">
+ <strong><a class="text-dark" href="https://www.gentoo.org/inside-gentoo/contact/">Contact</a></strong><br>
+ </div>
+ </div>
+ </div>
+ </footer>
+
+ <script
+ src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
+ integrity="sha256-4+XzXVhsDmqanXGHaHvgh1gMQKX40OUvDEBTu8JcmNs="
+ crossorigin="anonymous"></script>
+ <script src="https://assets.gentoo.org/tyrian/v2/popper.min.js"></script>
+ <script src="https://assets.gentoo.org/tyrian/v2/bootstrap.min.js"></script>
+{{end}}
diff --git a/web/templates/layout/head.tmpl b/web/templates/layout/head.tmpl
new file mode 100644
index 0000000..4ab796a
--- /dev/null
+++ b/web/templates/layout/head.tmpl
@@ -0,0 +1,12 @@
+{{define "head"}}
+ <head>
+ <title>Gentoo URL Shortener</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="theme-color" content="#54487a">
+ <meta name="description" content="Gentoo URL Shortener">
+ <link href="https://assets.gentoo.org/tyrian/v2/tyrian.min.css" rel="stylesheet" media="screen">
+ <link href="/assets/application.css" rel="stylesheet" media="screen">
+ <link rel="icon" href="https://packages.gentoo.org/favicon.ico" type="image/x-icon">
+ </head>
+{{end}}
diff --git a/web/templates/layout/header.tmpl b/web/templates/layout/header.tmpl
new file mode 100644
index 0000000..571a58b
--- /dev/null
+++ b/web/templates/layout/header.tmpl
@@ -0,0 +1,6 @@
+{{define "header"}}
+ <header>
+ {{template "sitetitle"}}
+ {{template "tyrian-navbar" .}}
+ </header>
+{{end}}
diff --git a/web/templates/layout/sitetitle.tmpl b/web/templates/layout/sitetitle.tmpl
new file mode 100644
index 0000000..bd07b34
--- /dev/null
+++ b/web/templates/layout/sitetitle.tmpl
@@ -0,0 +1,37 @@
+{{define "sitetitle"}}
+ <div class="site-title">
+ <div class="container">
+ <div class="row justify-content-between">
+ <div class="logo">
+ <a href="/" title="Back to the homepage" class="site-logo">
+ <img src="https://assets.gentoo.org/tyrian/site-logo.png" alt="Gentoo" srcset="https://assets.gentoo.org/tyrian/site-logo.svg">
+ </a>
+ <span class="site-label">URL Shortener</span>
+ </div>
+ <div class="site-title-buttons">
+ <div class="btn-group btn-group-sm">
+ <a href="https://get.gentoo.org/" role="button" class="btn get-gentoo"><span class="fa fa-fw fa-download"></span> <strong>Get Gentoo!</strong></a>
+ <div class="btn-group btn-group-sm">
+ <a class="btn gentoo-org-sites dropdown-toggle" data-toggle="dropdown" data-target="#" href="#">
+ <span class="fa fa-fw fa-map-o"></span> <span class="d-none d-sm-inline">gentoo.org sites</span> <span class="caret"></span>
+ </a>
+ <div class="dropdown-menu dropdown-menu-right">
+ <a class="dropdown-item" href="https://www.gentoo.org/" title="Main Gentoo website"><span class="fa fa-home fa-fw"></span> gentoo.org</a>
+ <a class="dropdown-item" href="https://wiki.gentoo.org/" title="Find and contribute documentation"><span class="fa fa-file-text-o fa-fw"></span> Wiki</a>
+ <a class="dropdown-item" href="https://bugs.gentoo.org/" title="Report issues and find common issues"><span class="fa fa-bug fa-fw"></span> Bugs</a>
+ <a class="dropdown-item" href="https://forums.gentoo.org/" title="Discuss with the community"><span class="fa fa-comments-o fa-fw"></span> Forums</a>
+ <a class="dropdown-item" href="https://packages.gentoo.org/" title="Find software for your Gentoo"><span class="fa fa-hdd-o fa-fw"></span> Packages</a>
+ <div class="dropdown-divider"></div>
+ <a class="dropdown-item" href="https://planet.gentoo.org/" title="Find out what's going on in the developer community"><span class="fa fa-rss fa-fw"></span> Planet</a>
+ <a class="dropdown-item" href="https://archives.gentoo.org/" title="Read up on past discussions"><span class="fa fa-archive fa-fw"></span> Archives</a>
+ <a class="dropdown-item" href="https://sources.gentoo.org/" title="Browse our source code"><span class="fa fa-code fa-fw"></span> Sources</a>
+ <div class="dropdown-divider"></div>
+ <a class="dropdown-item" href="https://infra-status.gentoo.org/" title="Get updates on the services provided by Gentoo"><span class="fa fa-server fa-fw"></span> Infra Status</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+{{end}}
diff --git a/web/templates/layout/tyriannav.tmpl b/web/templates/layout/tyriannav.tmpl
new file mode 100644
index 0000000..6e1de17
--- /dev/null
+++ b/web/templates/layout/tyriannav.tmpl
@@ -0,0 +1,34 @@
+{{define "tyrian-navbar"}}
+ <nav class="tyrian-navbar navbar navbar-dark navbar-expand-lg bg-primary" role="navigation">
+ <div class="container">
+ <div class="navbar-header">
+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-main-collapse" aria-controls="navbar-main-collapse" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ </div>
+ <div class="collapse navbar-collapse navbar-main-collapse" id="navbar-main-collapse">
+ <ul class="navbar-nav mr-auto">
+ <li class="nav-item {{if eq .Tab "create"}}active{{end}}"><a class="nav-link" href="/links/create">Shorten</a></li>
+ <li class="nav-item {{if eq .Tab "show"}}active{{end}}"><a class="nav-link" href="/links/show">Your Links</a></li>
+ </ul>
+
+ <ul class="navbar-nav">
+ {{if .User.IsAdmin }}
+ <li class="nav-item {{if eq .Tab "admin"}}active{{end}}"><a class="nav-link" href="/admin/">Admin</a></li>
+ {{end}}
+ <li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+ <img style="border-radius: 50%;height:20px;border: 1px solid white;margin-right:5px;" src="{{gravatar .User.Email}}" />
+ </a>
+ <div class="dropdown-menu" aria-labelledby="navbarDropdown">
+ <a class="dropdown-item disabled">Signed in as <b>{{.User.UserName}}</b></a>
+ <div class="dropdown-divider"></div>
+ <a class="dropdown-item" href="/auth/logout">Logout</a>
+ </div>
+ </li>
+ </ul>
+
+ </div>
+ </div>
+ </nav>
+{{end}}
diff --git a/web/templates/links/create.tmpl b/web/templates/links/create.tmpl
new file mode 100644
index 0000000..3644e08
--- /dev/null
+++ b/web/templates/links/create.tmpl
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html lang="en">
+{{template "head" .}}
+<body>
+{{template "header" .}}
+
+<div class="container mb-5 h-100">
+ <div class="row align-items-center h-100">
+ <div class="col-12 pb-5 mb-5">
+
+ <div class="jumbotron my-auto" style="background: none!important;">
+ <h2 style="font-size: 3.25em;text-align: center;margin-bottom: 0.75em;margin-top: 0;font-weight: 500;" class="site-welcome stick-top">Welcome <span style="color: #54487A !important;">{{.User.UserName}}</span>!<br>
+ Shorten your next link</h2>
+
+ <form action="/links/create" method="post">
+ <div class="typeahead-container px-5" style="font-family: 'Open Sans',Arial,Helvetica,Sans-Serif;position: relative;">
+ <div class="row">
+ <div class="col-12">
+ <div class="typeahead-field" style="display: table;border-collapse: separate;box-sizing: border-box;position: relative;width: 100%;">
+ <span class="typeahead-query" style="font-size: 1.1em; height: 2.3em;display: table-cell;vertical-align: top;width: 100%;">
+ <input class="rounded" style="border: 1px solid #ccc;padding-left:10px;font-size: 1.3em; height: 2.5em;" id="target" name="target" type="search" autocomplete="off" placeholder="Paste your link" aria-label="Paste your link" autofocus="">
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div class="collapse" id="collapseExample">
+ <div class="row mt-4">
+ <div class="col-5">
+ <div class="form-group">
+ <label for="prefix">Prefix</label>
+ <select class="form-control" name="prefix" id="prefix" onchange="togglePrefix();">
+ <option value="">/</option>
+ <option value="{{$.User.UserName}}">/{{$.User.UserName}}/</option>
+ {{range $index, $project := .User.Projects}}
+ <option value="{{$project}}">/{{$project}}/</option>
+ {{end}}
+ </select>
+ </div>
+ </div>
+ <div class="col-2">
+ <div class="form-group">
+ <label for="index">Index</label>
+ <select class="form-control" name="index" id="index" disabled>
+ <option value="no" selected>no</option>
+ <option value="yes">yes</option>
+ </select>
+ </div>
+ </div>
+ <div class="col-5">
+ <div class="form-group">
+ <label for="token">Custom Link</label>
+ <input class="form-control" name="token" id="token" placeholder="Only available for prefix != '/'" disabled>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row mt-4">
+ <div class="col-12">
+ <button class="float-right btn btn-primary">Shorten</button>
+ <button class="float-right btn btn-link text-dark" type="button" data-toggle="collapse" data-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample">
+ Customize
+ </button>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ </div>
+ </div>
+</div>
+
+
+{{template "footer" .}}
+
+<script src="/assets/application.js"></script>
+
+</body>
+</html>
diff --git a/web/templates/links/created.tmpl b/web/templates/links/created.tmpl
new file mode 100644
index 0000000..e3f93a5
--- /dev/null
+++ b/web/templates/links/created.tmpl
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html lang="en">
+{{template "head" .}}
+<body>
+{{template "header" .}}
+
+<div class="container mb-5 h-100">
+ <div class="row align-items-center h-100">
+ <div class="col-12 pb-5 mb-5">
+
+ <div class="jumbotron my-auto" style="background: none!important;">
+ <h2 style="font-size: 3.25em;text-align: center;margin-bottom: 0.75em;margin-top: 0;font-weight: 500;" class="site-welcome stick-top">Congratulations <span style="color: #54487A !important;">{{.User.UserName}}</span>!<br>
+ Your link has been shortened</h2>
+
+ <div class="typeahead-container px-5" style="font-family: 'Open Sans',Arial,Helvetica,Sans-Serif;position: relative;">
+ <div class="row">
+ <div class="col-12">
+ <div class="typeahead-field" style="display: table;border-collapse: separate;box-sizing: border-box;position: relative;width: 100%;">
+ <span class="typeahead-query" style="font-size: 1.1em; height: 2.3em;display: table-cell;vertical-align: top;width: 100%;">
+ <input class="rounded" style="border: 1px solid #ccc;padding-left:10px;font-size: 1.3em; height: 2.5em;" id="shortlink" name="shortlink" type="search" autocomplete="off" aria-label="Shortened Link" autofocus="" value="{{.ShortLink}}" readonly>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div id="copied" class="col-12 text-right text-primary" style="display: none;">
+ Successfully copied to the Clipboard.
+ </div>
+ </div>
+
+ <div class="row mt-4">
+ <div class="col-12">
+ <button data-clipboard-target="#shortlink" onclick="document.getElementById('copied').style.display = 'block';" class="float-right btn btn-primary">Copy to Clipbaord</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+</div>
+
+{{template "footer" .}}
+
+<script src="/assets/clipboard.min.js"></script>
+<script>
+ new ClipboardJS('.btn');
+</script>
+
+</body>
+</html>
diff --git a/web/templates/links/show.tmpl b/web/templates/links/show.tmpl
new file mode 100644
index 0000000..de961f5
--- /dev/null
+++ b/web/templates/links/show.tmpl
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html lang="en">
+{{template "head" .}}
+<body>
+{{template "header" .}}
+
+<div class="container mb-5">
+ <div class="row">
+ <div class="col-12">
+
+ <h2 class="mt-4"><span style="font-family: monospace;">/</span></h2>
+
+ {{$empty := true}}
+
+ <table class="table">
+ <colgroup>
+ <col span="1" style="width: 20%;">
+ <col span="1" style="width: 60%;">
+ <col span="1" style="width: 10%;">
+ <col span="1" style="width: 5%;">
+ <col span="1" style="width: 5%;">
+ </colgroup>
+ <thead>
+ <tr>
+ <th scope="col">Short URL</th>
+ <th scope="col">Target</th>
+ <th scope="col">Creator</th>
+ <th scope="col">Hits</th>
+ <th scope="col">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{$empty = true}}
+ {{range .UserLinks}}
+ {{if eq .Prefix ""}}
+ {{$empty = false}}
+ <tr>
+ <th scope="row"><a class="text-dark" href="{{.ShortLink}}">go.gentoo.org{{.ShortLink}}</a></th>
+ <td><a class="text-dark" href="{{.TargetLink}}">{{.TargetLink}}</a></td>
+ <td>{{replaceAll .UserEmail "@gentoo.org" ""}}</td>
+ <td>{{.Hits}}</td>
+ <td>
+ <form action="/links/delete" method="post">
+ <input hidden name="prefix" value="{{.Prefix}}">
+ <input hidden name="token" value="{{.URLToken}}">
+ <input hidden name="from" value="/links/show">
+ <button type="submit" class="btn btn-link py-0 text-dark">
+ <i class="fa fa-trash-o" aria-hidden="true"></i>
+ </button>
+ </form>
+ </td>
+ </tr>
+ {{end}}
+ {{end}}
+ {{if $empty}}
+ <tr>
+ <td colspan="5" class="text-center"><i>No shortened URLs yet</i></td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+
+ <h2 class="mt-5"><span style="font-family: monospace;">/{{.User.UserName}}/</span></h2>
+
+ <table class="table">
+ <colgroup>
+ <col span="1" style="width: 20%;">
+ <col span="1" style="width: 60%;">
+ <col span="1" style="width: 10%;">
+ <col span="1" style="width: 5%;">
+ <col span="1" style="width: 5%;">
+ </colgroup>
+ <thead>
+ <tr>
+ <th scope="col">Short URL</th>
+ <th scope="col">Target</th>
+ <th scope="col">Creator</th>
+ <th scope="col">Hits</th>
+ <th scope="col">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{$empty = true}}
+ {{range .UserLinks}}
+ {{if eq .Prefix $.User.UserName}}
+ {{$empty = false}}
+ <tr>
+ <th scope="row"><a class="text-dark" href="{{.ShortLink}}">go.gentoo.org{{.ShortLink}}</a></th>
+ <td><a class="text-dark" href="{{.TargetLink}}">{{.TargetLink}}</a></td>
+ <td>{{replaceAll .UserEmail "@gentoo.org" ""}}</td>
+ <td>{{.Hits}}</td>
+ <td>
+ <form action="/links/delete" method="post">
+ <input hidden name="prefix" value="{{.Prefix}}">
+ <input hidden name="token" value="{{.URLToken}}">
+ <input hidden name="from" value="/links/show">
+ <button type="submit" class="btn btn-link py-0 text-dark">
+ <i class="fa fa-trash-o" aria-hidden="true"></i>
+ </button>
+ </form>
+ </td>
+ </tr>
+ {{end}}
+ {{end}}
+ {{if $empty}}
+ <tr>
+ <td colspan="4" class="text-center"><i>No shortened URLs yet</i></td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+
+ <div class="separator mt-5">Projects</div>
+
+ {{range $index, $project := .User.Projects}}
+ <h2 class="mt-5"><span style="font-family: monospace;">/{{$project}}/</span></h2>
+
+ <table class="table">
+ <colgroup>
+ <col span="1" style="width: 20%;">
+ <col span="1" style="width: 60%;">
+ <col span="1" style="width: 10%;">
+ <col span="1" style="width: 5%;">
+ <col span="1" style="width: 5%;">
+ </colgroup>
+ <thead>
+ <tr>
+ <th scope="col">Short URL</th>
+ <th scope="col">Target</th>
+ <th scope="col">Creator</th>
+ <th scope="col">Hits</th>
+ <th scope="col">Action</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{$empty = true}}
+ {{range $.UserLinks}}
+ {{if eq .Prefix $project}}
+ {{$empty = false}}
+ <tr>
+ <th scope="row"><a class="text-dark" href="{{.ShortLink}}">go.gentoo.org{{.ShortLink}}</a></th>
+ <td><a class="text-dark" href="{{.TargetLink}}">{{.TargetLink}}</a></td>
+ <td>{{replaceAll .UserEmail "@gentoo.org" ""}}</td>
+ <td>{{.Hits}}</td>
+ <td>
+ <form action="/links/delete" method="post">
+ <input hidden name="prefix" value="{{.Prefix}}">
+ <input hidden name="token" value="{{.URLToken}}">
+ <input hidden name="from" value="/links/show">
+ <button type="submit" class="btn btn-link py-0 text-dark">
+ <i class="fa fa-trash-o" aria-hidden="true"></i>
+ </button>
+ </form>
+ </td>
+ </tr>
+ {{end}}
+ {{end}}
+ {{if $empty}}
+ <tr>
+ <td colspan="4" class="text-center"><i>No shortened URLs yet</i></td>
+ </tr>
+ {{end}}
+ </tbody>
+ </table>
+ {{end}}
+ </div>
+ </div>
+</div>
+
+{{template "footer" .}}
+
+</body>
+</html>