From 11b178629423bdad39a2b95752f47be0891bb9ed Mon Sep 17 00:00:00 2001 From: Guilhem CARRON <gcarron@grandlyon.com> Date: Fri, 24 Sep 2021 12:57:06 +0000 Subject: [PATCH] featt: Add backoffice first version with monthlyreport and authentification --- .gitignore | 2 + .gitlab-ci.yml | 34 ++ .vscode/launch.json | 69 +++ Dockerfile | 70 +++ README.md | 30 +- configs/tokenskey.json | 3 + dev_certificates/RootCA.crt | 20 + dev_certificates/RootCA.key | 28 + dev_certificates/RootCA.pem | 20 + dev_certificates/RootCA.srl | 1 + dev_certificates/domains.ext | 6 + dev_certificates/generate-certificates.sh | 6 + dev_certificates/localhost.crt | 21 + dev_certificates/localhost.csr | 17 + dev_certificates/localhost.key | 28 + docker-compose.yml | 61 ++ docs/docs.go | 694 ++++++++++++++++++++++ docs/swagger.json | 626 +++++++++++++++++++ docs/swagger.yaml | 416 +++++++++++++ go.mod | 13 + go.sum | 465 +++++++++++++++ internal/auth/auth.go | 215 +++++++ internal/auth/oauth2.go | 143 +++++ internal/common/common.go | 173 ++++++ internal/common/common_test.go | 86 +++ internal/file/file.go | 38 ++ internal/file/file_test.go | 38 ++ internal/mocks/mocks.go | 71 +++ internal/models/models.go | 45 ++ internal/models/monthlyInfo.go | 155 +++++ internal/models/monthlyNews.go | 155 +++++ internal/models/monthlyReport.go | 96 +++ internal/models/poll.go | 152 +++++ internal/rootmux/rootmux.go | 69 +++ internal/rootmux/rootmux_test.go | 196 ++++++ internal/tester/tester.go | 92 +++ internal/tokens/tokens.go | 232 ++++++++ internal/tokens/tokens_test.go | 64 ++ main.go | 44 ++ template.env | 20 + 40 files changed, 4712 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .vscode/launch.json create mode 100644 Dockerfile create mode 100644 configs/tokenskey.json create mode 100644 dev_certificates/RootCA.crt create mode 100644 dev_certificates/RootCA.key create mode 100644 dev_certificates/RootCA.pem create mode 100644 dev_certificates/RootCA.srl create mode 100644 dev_certificates/domains.ext create mode 100755 dev_certificates/generate-certificates.sh create mode 100644 dev_certificates/localhost.crt create mode 100644 dev_certificates/localhost.csr create mode 100644 dev_certificates/localhost.key create mode 100644 docker-compose.yml create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/auth/auth.go create mode 100644 internal/auth/oauth2.go create mode 100644 internal/common/common.go create mode 100644 internal/common/common_test.go create mode 100644 internal/file/file.go create mode 100644 internal/file/file_test.go create mode 100644 internal/mocks/mocks.go create mode 100644 internal/models/models.go create mode 100644 internal/models/monthlyInfo.go create mode 100644 internal/models/monthlyNews.go create mode 100644 internal/models/monthlyReport.go create mode 100644 internal/models/poll.go create mode 100644 internal/rootmux/rootmux.go create mode 100644 internal/rootmux/rootmux_test.go create mode 100644 internal/tester/tester.go create mode 100644 internal/tokens/tokens.go create mode 100644 internal/tokens/tokens_test.go create mode 100644 main.go create mode 100644 template.env diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c09811 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +backoffice.db \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..abddb22 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,34 @@ +# +# Ce fichier doit être adapté en fonction du projet en renseignant les variables SONAR_PROJECT_KEY et SONAR_TOKEN dans la configuration graphique du projet (https://forge.grandlyon.com/<CHEMIN_DE_VOTRE_PROJET>/settings/ci_cd) +# La variable SONAR_PROJET_KEY peut être trouvée sur https://sonarqube.forge.grandlyon.com/dashboard en ouvrant le projet et en copiant collant le champ en bas à droite (Project Key) +# +# La variable SONAR_TOKEN doit être générée par le responsable du projet depuis son interface sonar : https://sonarqube.forge.grandlyon.com/account/security/ +# + +image: docker:git + +services: + - docker:dind + +variables: + DOCKER_DRIVER: overlay2 + DOCKER_TLS_CERTDIR: "" + GIT_STRATEGY: clone + GIT_DEPTH: 0 + +stages: + - build + +build: + image: docker:18.09 + services: + - docker:18.09-dind + stage: build + script: + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + - DOCKER_BUILDKIT=1 docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" . + - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG" + only: + - dev + - master + diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..37a6cd1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,69 @@ +{ + // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. + // Pointez pour afficher la description des attributs existants. + // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Back-office with Mock OAuth2", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${workspaceFolder}/main.go", + "env": { + "REDIRECT_URL": "https://localhost:1443/OAuth2Callback", + "CLIENT_ID": "foo", + "CLIENT_SECRET": "bar", + "AUTH_URL": "http://localhost:8090/auth", + "TOKEN_URL": "http://localhost:8090/token", + "USERINFO_URL": "http://localhost:8090/admininfo", + "LOGOUT_URL": "/", + "HOSTNAME": "localhost", + "ADMIN_ROLE" : "ADMINS", + "INMEMORY_TOKEN_LIFE_DAYS": "2", + "DEBUG_MODE": "true", + "HTTPS_PORT": "1443", + "DATABASE_USER": "root", + "DATABASE_PASSWORD": "password", + "DATABASE_NAME": "backoffice", + "DATABASE_HOST": "127.0.0.1", + }, + "showLog": true + }, + { + "name": "Debug Back-office with Sign&Go", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${workspaceFolder}/main.go", + "env": { + "REDIRECT_URL": "https://localhost:1443/OAuth2Callback", + "CLIENT_ID": "<GET ONE FROM YOUR IDP>", + "CLIENT_SECRET": "<GET ONE FROM YOUR IDP>", + "AUTH_URL": "https://connexion-rec.grandlyon.fr/IdPOAuth2/authorize/oidc-rec", + "TOKEN_URL": "https://connexion-rec.grandlyon.fr/IdPOAuth2/token/oidc-rec", + "USERINFO_URL": "https://connexion-rec.grandlyon.fr/IdPOAuth2/userinfo/oidc-rec", + "LOGOUT_URL": "https://connexion-rec.grandlyon.fr/auth/logout.jsp", + "ADMIN_ROLE": "GGD_ORG_DG-DEES-DINSI-DAAG_TOUS", + "HOSTNAME": "ecolyobackoffice.127.0.0.1.nip.io", + "DEBUG_MODE": "true", + "HTTPS_PORT": "1443" + }, + "showLog": true + }, + { + "name": "Debug back office client", + "type": "firefox", + "request": "launch", + "reAttach": true, + "url": "https://ecolyobackoffice.127.0.0.1.nip.io:1443", + "webRoot": "${workspaceFolder}/web" + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8aa2263 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# Dockerfile from https://github.com/chemidy/smallest-secured-golang-docker-image + +################################## +# STEP 1 build executable binary # +################################## + +FROM golang:alpine as builder + +# Install git + SSL ca certificates. +# Git is required for fetching the dependencies. +# Ca-certificates is required to call HTTPS endpoints. +RUN apk update && apk add --no-cache git ca-certificates tzdata libcap mailcap && update-ca-certificates +RUN apk add build-base + +# Create appuser +ENV USER=appuser +ENV UID=1000 +# See https://stackoverflow.com/a/55757473/12429735 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + "${USER}" + +WORKDIR /app + +ADD . . + +RUN chown -Rf "${UID}" ./* + +# Get dependencies and run tests +RUN go version +RUN go get -d -v +RUN CGO_ENABLED=1 go test ./... + +# Build the binary +RUN CGO_ENABLED=1 go build \ + -ldflags='-w -s -extldflags "-static"' -a \ + -o /app/backoffice-server . + +# Allow running on ports < 1000 +RUN setcap cap_net_bind_service=+ep /app/backoffice-server + +############################## +# STEP 2 build a small image # +############################## +FROM alpine:3.14.0 + +WORKDIR /app + +# Import global resources from builder +COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /etc/passwd /etc/passwd +COPY --from=builder /etc/group /etc/group +COPY --from=builder /etc/mime.types /etc/mime.types + +# Copy static executable and application resources +COPY --from=builder /app/backoffice-server /app/backoffice-server +COPY --from=builder /app/dev_certificates /app/dev_certificates +COPY --from=builder /app/configs /app/configs + +# Use an unprivileged user. +USER appuser:appuser + +# Run the binary +ENTRYPOINT ["./backoffice-server"] \ No newline at end of file diff --git a/README.md b/README.md index a6da557..0f6ecf8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,29 @@ -# Backoffice - Server +# Ecolyo Agent - Server -Backoffice server of ecolyo app +Ecolyo Agent is the backoffice for the Ecolyo app. + +This repository contains the backend part of this backoffice. + +# Features + +- Authentification using OAuth2 + +- Connected admins can use an API to edit and save different sections of the newsletters that will be sent to Ecolyo users + +- Exposes a public route to get the sections of a specific or the last newsletter + +# How to setup : + +This backend should be deployed with the frontend from [this repo](https://forge.grandlyon.com/web-et-numerique/llle_project/backoffice-client) + +However this backend can be run in standalone : + +- Clone the repository + +- Set a .env file at the root and add all variables declared in the [template.env](https://forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/-/blob/dev/template.env) file + +- (**Optionnal**) you can create new certificates by running `cd dev_certificates && ./generate-certificates.sh` + +- Run `docker-compose up -d` + +Once deployed, you can access to a Swagger documentation of the API on https://${HOSTNAME}/swagger/index.html diff --git a/configs/tokenskey.json b/configs/tokenskey.json new file mode 100644 index 0000000..3920d9a --- /dev/null +++ b/configs/tokenskey.json @@ -0,0 +1,3 @@ +{ + "Key": "2ioa6+gmlILIQcsG/HmBqDSsszPCe4GWKjCfyrtgLek=" +} \ No newline at end of file diff --git a/dev_certificates/RootCA.crt b/dev_certificates/RootCA.crt new file mode 100644 index 0000000..efcaf26 --- /dev/null +++ b/dev_certificates/RootCA.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDMzCCAhugAwIBAgIUW08A6nSohnq7VLjzATCAK8uJRH4wDQYJKoZIhvcNAQEL +BQAwKTELMAkGA1UEBhMCVVMxGjAYBgNVBAMMEVZlc3RpYnVsZS1Sb290LUNBMB4X +DTIxMDcwOTEwNDUzMVoXDTI0MDQyODEwNDUzMVowKTELMAkGA1UEBhMCVVMxGjAY +BgNVBAMMEVZlc3RpYnVsZS1Sb290LUNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAsGnISpIxiVdObomVjon7fWU2IkeWFZnjs47CdF9gitinIzn8j1bD +a6U3TrY490c8bXv1KGLNw3/4QN7svsQ0I+NTU4JTgO/5LYvFItUqC7NchG/XKM4y +VDWnmrR8Hm0RErMVInyM9Ww3YgX5nn6dUI6u8isvydL66mHkL340Ej0T/F/eyiqU +LL5mkRAg+6nvdh2kfLqbqGKAN6cQxISLfnMNUf8IqKGE6FaPw8C3xKR1ModQBvcJ +pjeCCefJN+tp6UBJSNakhwxUOcSc8ogb5d1FHm8cbAmf1yubNpcKWzcenPRDJPzK +vRThD775FGQKO3ZMJg7neekPYQei4DR/HwIDAQABo1MwUTAdBgNVHQ4EFgQUtxCV +RcrVAGPYKayZkktjJTtiAQMwHwYDVR0jBBgwFoAUtxCVRcrVAGPYKayZkktjJTti +AQMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAe3n9IVVWm//M +oYpCLjqwVFdXh22v0jf5RB5wQvDYnBlQPmh0g37TVwsSHLNkVmCS6q6NdAewbd52 +mrthFuZ8BSHNWcevZYeyKo7Ji3UkF9sECAxiPmd9tMDxG5zqCCEfU0qcSHDY8AC1 +R68Z20jMpx8PjJN3hQtG77J66YB2eXy0DGO8NzB5aBrD9s/nrm9CiVkysFUJaCEk +rN2A2w2mAcXsCbi33GvPe06EnAjTH4DUZrd4buPYSWmHnalHi9GLH4/3hkNBroux +iVrfXW3W/aeYiYb8snbFSwKniB+1V4wtDJenhPzXZOjWb1TiPqOJYLrMsVsXt/6b ++nnB7t6tMA== +-----END CERTIFICATE----- diff --git a/dev_certificates/RootCA.key b/dev_certificates/RootCA.key new file mode 100644 index 0000000..b6dca61 --- /dev/null +++ b/dev_certificates/RootCA.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwachKkjGJV05u +iZWOift9ZTYiR5YVmeOzjsJ0X2CK2KcjOfyPVsNrpTdOtjj3Rzxte/UoYs3Df/hA +3uy+xDQj41NTglOA7/kti8Ui1SoLs1yEb9cozjJUNaeatHwebRESsxUifIz1bDdi +Bfmefp1Qjq7yKy/J0vrqYeQvfjQSPRP8X97KKpQsvmaRECD7qe92HaR8upuoYoA3 +pxDEhIt+cw1R/wiooYToVo/DwLfEpHUyh1AG9wmmN4IJ58k362npQElI1qSHDFQ5 +xJzyiBvl3UUebxxsCZ/XK5s2lwpbNx6c9EMk/Mq9FOEPvvkUZAo7dkwmDud56Q9h +B6LgNH8fAgMBAAECggEAYaHJlWufOrE02PwP5xj6NAXFleckasQGPqNtftYiKfWJ +WneHDRUphfOjzk4O7Nth1/3YSgeUdPPnwo8dWt5fNNVkkjz4Vc15i/lkmsh6Qot7 +UlhLLWwgcnZXUck9P+GAp2aw9asUn+bKJ9fCtDLCgYjVzXSVOA8pinmuvZIM42I6 +g164YSMMgdz0DleHArmK9RW4S5v7mlOeF0FSxBxwuja1qOeFylMMqNn7IRP+UWOo +AKu0YszT9uqcAqih8jNDvPv7vNVzVmJSSjwKRLQ7QBmfIPnlf3o42+oc2Winiqac +8i5ODjpKox38H47u+Viy4vA5nWmXOYqXyZDE1eJ7EQKBgQDhRm9kMVKr+9ccmZO2 +u+JZSUDD2dYGTfBdewU6vLbmNUhYv7Mlcme9a0+FSn/PSW2HAqSPTp5BQ6bQiHME +G+Ve0wCQhwXML4zRFV58Ooq2GKvSoHbkptCmmxcT+I4Ef9j6Q1wtWn4QSpF7fZ49 +PONkfT8d5GTkTPRJoyU5id56GwKBgQDIeVCnxTcRXKr/opCiXB8RYNk5ySkNovPy +WpzEpKQ8ibJWDRYSNMtE/1+UK9FEJKOhbySnk0hnAyJ3ZNLZ5Z/wsAUwsqy4WvWB +jqZ+rABkAaSy1IYZEl7RL4Yd/oPih1FiN2GfYRmUHIpRsr5IVwDzBLulBRixt15E +xg3JxomfTQKBgQCmcco/twmkNND9OqOfMjbNTYhirIKr6c4c45Y7jc99TAUGPa15 +j4wCslTw4NiKKXCZfmRj1eyrv/ZywT5p3MqeQzx5jKnF8aQTn7xOAVsXrCbX2uO9 +kVs8nf5xLQaRYHzKfBaRE/lsxAu1uFzAVkqUps2JooTBAfLErZwFZU5R3QKBgE6L +YXNqDGpMAV9JBRvntfBsHo/KZcBHAQcKQ3O0AfkKBgo69FPLxXxSBdOa21G0fTvJ +vPW++dYKX12h7g6bLe/yNwZeateMI7ZP+qGUqE6Gak36gFOgY+/Xi9eCmY+Obu9p +PWFhfNEP4Y2i13SmSePtDcvY1FUEv/V4F3zfwZndAoGASst6PPyVWY84uuib3Rwu +p4btHMuRRoXc4o5mBNfnVRy9vE7HMuFyk3l7bQ375C68iv7FWIJV90y0DBWgtOUU +yb8kUGe1VNAKijhA0DU7BRJkV2hQiXAR6nIrNuSn8J/41UyBpYxf30T0xGgh/qqY +zxf//ghXSRHPuAzaf1LGJrQ= +-----END PRIVATE KEY----- diff --git a/dev_certificates/RootCA.pem b/dev_certificates/RootCA.pem new file mode 100644 index 0000000..efcaf26 --- /dev/null +++ b/dev_certificates/RootCA.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDMzCCAhugAwIBAgIUW08A6nSohnq7VLjzATCAK8uJRH4wDQYJKoZIhvcNAQEL +BQAwKTELMAkGA1UEBhMCVVMxGjAYBgNVBAMMEVZlc3RpYnVsZS1Sb290LUNBMB4X +DTIxMDcwOTEwNDUzMVoXDTI0MDQyODEwNDUzMVowKTELMAkGA1UEBhMCVVMxGjAY +BgNVBAMMEVZlc3RpYnVsZS1Sb290LUNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAsGnISpIxiVdObomVjon7fWU2IkeWFZnjs47CdF9gitinIzn8j1bD +a6U3TrY490c8bXv1KGLNw3/4QN7svsQ0I+NTU4JTgO/5LYvFItUqC7NchG/XKM4y +VDWnmrR8Hm0RErMVInyM9Ww3YgX5nn6dUI6u8isvydL66mHkL340Ej0T/F/eyiqU +LL5mkRAg+6nvdh2kfLqbqGKAN6cQxISLfnMNUf8IqKGE6FaPw8C3xKR1ModQBvcJ +pjeCCefJN+tp6UBJSNakhwxUOcSc8ogb5d1FHm8cbAmf1yubNpcKWzcenPRDJPzK +vRThD775FGQKO3ZMJg7neekPYQei4DR/HwIDAQABo1MwUTAdBgNVHQ4EFgQUtxCV +RcrVAGPYKayZkktjJTtiAQMwHwYDVR0jBBgwFoAUtxCVRcrVAGPYKayZkktjJTti +AQMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAe3n9IVVWm//M +oYpCLjqwVFdXh22v0jf5RB5wQvDYnBlQPmh0g37TVwsSHLNkVmCS6q6NdAewbd52 +mrthFuZ8BSHNWcevZYeyKo7Ji3UkF9sECAxiPmd9tMDxG5zqCCEfU0qcSHDY8AC1 +R68Z20jMpx8PjJN3hQtG77J66YB2eXy0DGO8NzB5aBrD9s/nrm9CiVkysFUJaCEk +rN2A2w2mAcXsCbi33GvPe06EnAjTH4DUZrd4buPYSWmHnalHi9GLH4/3hkNBroux +iVrfXW3W/aeYiYb8snbFSwKniB+1V4wtDJenhPzXZOjWb1TiPqOJYLrMsVsXt/6b ++nnB7t6tMA== +-----END CERTIFICATE----- diff --git a/dev_certificates/RootCA.srl b/dev_certificates/RootCA.srl new file mode 100644 index 0000000..cdd51ea --- /dev/null +++ b/dev_certificates/RootCA.srl @@ -0,0 +1 @@ +3C13A2E5C7F49006B6843A514AD3AD7E1FC88B9B diff --git a/dev_certificates/domains.ext b/dev_certificates/domains.ext new file mode 100644 index 0000000..0bba95d --- /dev/null +++ b/dev_certificates/domains.ext @@ -0,0 +1,6 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost diff --git a/dev_certificates/generate-certificates.sh b/dev_certificates/generate-certificates.sh new file mode 100755 index 0000000..98a5266 --- /dev/null +++ b/dev_certificates/generate-certificates.sh @@ -0,0 +1,6 @@ +#!/bin/bash +rm -f *.crt *.csr *.key *.pem *.srl +openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout RootCA.key -out RootCA.pem -subj "/C=US/CN=EcolyoBackOffice-Root-CA" +openssl x509 -outform pem -in RootCA.pem -out RootCA.crt +openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=EcolyoBackOffice-Certificates/CN=localhost.local" +openssl x509 -req -sha256 -days 1024 -in localhost.csr -CA RootCA.pem -CAkey RootCA.key -CAcreateserial -extfile domains.ext -out localhost.crt diff --git a/dev_certificates/localhost.crt b/dev_certificates/localhost.crt new file mode 100644 index 0000000..49ccddb --- /dev/null +++ b/dev_certificates/localhost.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUPBOi5cf0kAa2hDpRStOtfh/Ii5swDQYJKoZIhvcNAQEL +BQAwKTELMAkGA1UEBhMCVVMxGjAYBgNVBAMMEVZlc3RpYnVsZS1Sb290LUNBMB4X +DTIxMDcwOTEwNDUzMVoXDTI0MDQyODEwNDUzMVowbzELMAkGA1UEBhMCVVMxEjAQ +BgNVBAgMCVlvdXJTdGF0ZTERMA8GA1UEBwwIWW91ckNpdHkxHzAdBgNVBAoMFlZl +c3RpYnVsZS1DZXJ0aWZpY2F0ZXMxGDAWBgNVBAMMD2xvY2FsaG9zdC5sb2NhbDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALLU4bOy9xtFmTVCUxuCCCGm +WFMgPlfFzwzZGqGN0cGvuynEUTcwgoyjYHidPC+k4q8/H/0IiOwBQSbOqm6D96YH +t74veeeudCmkM5ilemwba2zK11PDRTjw+91r3vZyQv/d5lyDhoBDeC6YD2Nf6MF7 +BP8wIAPwQksDK43AdGxEv6tPIHQ54exEpB9zfqcY1afSR6OhLDq0CEg04CKi6B/y +YLMaVa9RH+UeAUX7VSuJrCNgCxj950YVVIJ1jizkgodhdL4HMbRTXLK6hPpEqn5a +XuV4OOGlmnDn3o+UJAmmZj4LiSVCY2PPpOhOX7CJgwUEhM1XgelLFdUrKVhVJlsC +AwEAAaNRME8wHwYDVR0jBBgwFoAUtxCVRcrVAGPYKayZkktjJTtiAQMwCQYDVR0T +BAIwADALBgNVHQ8EBAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3 +DQEBCwUAA4IBAQAl7lHURZytJ1eO5AUiYa/4HqmlIwdV3JzEUpDbLcLdUQOEz95s +56CDh1iji1RWA+8RMOJlgnWptG3HHBXkzsQPYMlI9ofpbq4ZGCLh6ZWk1OsRnj6l +d4gyWlobf0WgVLuFo1vQZrzm5zDbAaVG2cj0avdhaLiMMWpneqdx9Zfn/QIvh+AB +IBN8bUIEylbNUmZDFbVImyoDC0pbnnsp5Qn3wLL1RyIJhXF9O84M/htOPFs0t0s1 +ZsQKrkG9/TqOPfFePjNbgWcvVGvsTKfX9/B1Uyvpr2ko9quj5Q0UvjOm2Dypi/fe +Vk1+m4gEWo4P3SyOXTcU8nX1EmiLeyLaLpcQ +-----END CERTIFICATE----- diff --git a/dev_certificates/localhost.csr b/dev_certificates/localhost.csr new file mode 100644 index 0000000..29522a4 --- /dev/null +++ b/dev_certificates/localhost.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICtDCCAZwCAQAwbzELMAkGA1UEBhMCVVMxEjAQBgNVBAgMCVlvdXJTdGF0ZTER +MA8GA1UEBwwIWW91ckNpdHkxHzAdBgNVBAoMFlZlc3RpYnVsZS1DZXJ0aWZpY2F0 +ZXMxGDAWBgNVBAMMD2xvY2FsaG9zdC5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALLU4bOy9xtFmTVCUxuCCCGmWFMgPlfFzwzZGqGN0cGvuynE +UTcwgoyjYHidPC+k4q8/H/0IiOwBQSbOqm6D96YHt74veeeudCmkM5ilemwba2zK +11PDRTjw+91r3vZyQv/d5lyDhoBDeC6YD2Nf6MF7BP8wIAPwQksDK43AdGxEv6tP +IHQ54exEpB9zfqcY1afSR6OhLDq0CEg04CKi6B/yYLMaVa9RH+UeAUX7VSuJrCNg +Cxj950YVVIJ1jizkgodhdL4HMbRTXLK6hPpEqn5aXuV4OOGlmnDn3o+UJAmmZj4L +iSVCY2PPpOhOX7CJgwUEhM1XgelLFdUrKVhVJlsCAwEAAaAAMA0GCSqGSIb3DQEB +CwUAA4IBAQCSS3TfTAsUsGYTXfnJ/w1+2KCatkQBbU9cUrxalQrCdTPu+sDeG+OL +1YRl27hzarN8UXwtEPV1VZNs7kAWdm3ndwy6Aqd5oTenC47bzPFwu3USioSwIyVI +h7c7TrryFUSJDXSNRXDuLoIaPCx9ZSkY0xkbCW+thiuWP0tANFmkAfo92EGBhQmp +g59Tn0A4ZveQBi0zHTHVMI2Y73FxA7pyyU1+eGAlBMqV/t6ipA0s5qKsVqXNJyRE +QgR9FRYZ0nJwiIKlM3YMFwpIu23xMHV3LxN8DjnsZzGzLCkvHja9E2gLtKu3wrRj +D1Ce/P/2hc4BWInuDIbA++b6O8oeWFq+ +-----END CERTIFICATE REQUEST----- diff --git a/dev_certificates/localhost.key b/dev_certificates/localhost.key new file mode 100644 index 0000000..709c5c5 --- /dev/null +++ b/dev_certificates/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCy1OGzsvcbRZk1 +QlMbggghplhTID5Xxc8M2RqhjdHBr7spxFE3MIKMo2B4nTwvpOKvPx/9CIjsAUEm +zqpug/emB7e+L3nnrnQppDOYpXpsG2tsytdTw0U48Pvda972ckL/3eZcg4aAQ3gu +mA9jX+jBewT/MCAD8EJLAyuNwHRsRL+rTyB0OeHsRKQfc36nGNWn0kejoSw6tAhI +NOAiougf8mCzGlWvUR/lHgFF+1UriawjYAsY/edGFVSCdY4s5IKHYXS+BzG0U1yy +uoT6RKp+Wl7leDjhpZpw596PlCQJpmY+C4klQmNjz6ToTl+wiYMFBITNV4HpSxXV +KylYVSZbAgMBAAECggEBAJeMFCDmvsXbuzQJGsiq7x0lDKCVKH/VX9sxeIID3wfW +VjPU7EZq05c/NJfCF6kAgCGmywLxYqctgPrUFFZHe2y2CZ4gOZx+mG5ZemgXg6Ft +syGk28leJ6FThv1jVrVeqyyN7ZPk2eyEQKqrrg62zlZ4XAmtzNPJnURYUWZ0+7Rr +Ozl5zQ458ebxKzvbHSkUyp6hlT8MWf2Nnh2Bu5EIdV2xibU1rjP+FSok6Jl3EwGq +O9nD70E4NMr2ReRPgwHVpTQ/U/eAyK4OO3E0CXMSX/K+/3vGVb/Urbdf9NneSXbT +w271vXHAtGFOaUkd19xykjP5iYnAEazHau4mswFDV0ECgYEA4IZnz/bh9s/8tMOw +QGULWa9lzWpas2YV1TSLD64GNYFjIN11rQCJ/hNvYfLaRHs0qwZGm+mOxZqU9Vvi +XRry1alGAn288P27+3RukeBRx2b7Ho2l9pNgWtYpkWhsOrMiTuPmyBr3eLb4E9RU +TRt1foz9GuqPW0QIaq8nRctpDxECgYEAy+apdlekhufwtsuydchaoScnSkE7qaLK +ot6QJzFXcYPD5r1ROv9jAAo4rBKgPOiUhANVXvkeZsT+/VEffP2o0DtapBWx+VPM +GI4IkMknsDT6lsapQWbKdGHrY9nJGRiT4cvOrzaSBYVFVWWfoWs46vLKCU13BJig +HzwrgaVStqsCgYEAqA1R1IHofdENR8uUt44p4bX7z7WEL/T/8HYEg/bwZMn0hVvd +QWE+5/JqEvkvz8QcFsp6vSYim9rpFYDxvFh4W934LdMpQYPZWQu72un4q/Rzj1nc +V+PVYggcUt7C62i7DCteyHYOtsbUhhsOAizEU7V5mNTp+hjA6AEztvTaLNECgYEA +yg5aABv5vnY54+sXfgB9TxUtqjfal8/qXluPHkeXD7Yze4Q/6ucJhBCc+Ge8wp74 +DZoAD41uwwiUZxLs0T/M+gzXVaLqKtkPd4XIlzG/Uq4tZRyYvWbPWWVvjhNTZLsm +UKtWteqt6SqX+ngqKBvI24qdC3roZnWYt1s5AdCCluECgYAxHYJ8C7CJRmLps1At +yml7L0KCKzmeDmbHdejhbO2/rKGL42Uua9NfOAKT268az6eCOLjcMeRdC7tKmS7K +8Z5QYYXY7WOnMOijBiBeowkx3oqpwh1N/PmxExMufStusGZMymrQSQXYWGSQ/ey3 +NXnceAUytLBRTy9zam0jqHHprQ== +-----END PRIVATE KEY----- diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d141301 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,61 @@ +version: '3.1' + +services: + database: + image: mysql:5 + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD} + MYSQL_DATABASE: ${DATABASE_NAME} + healthcheck: + test: mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD + interval: 5s + timeout: 10s + retries: 60 + + phpmyadmin: + image: phpmyadmin/phpmyadmin:latest + depends_on: + - database + ports: + - 8008:80 + environment: + PMA_HOST: database + + backoffice-container: + image: backoffice + depends_on: + database: + condition: service_healthy + build: . + restart: unless-stopped + volumes: + - /etc/localtime:/etc/localtime:ro + - ./configs:/app/configs + - ./letsencrypt_cache:/app/letsencrypt_cache + - ./data:/app/data + - ./../${IMAGE_FOLDER}:/app/${IMAGE_FOLDER} + ports: + - ${HTTPS_PORT}:${HTTPS_PORT} + - 8090:8090 + environment: + - HOSTNAME=${HOSTNAME} + - HTTPS_PORT=${HTTPS_PORT} + - ADMIN_ROLE=${ADMIN_ROLE} + - REDIRECT_URL=${REDIRECT_URL} + - IMAGE_FOLDER=${IMAGE_FOLDER} + - CLIENT_ID=${CLIENT_ID} + - CLIENT_SECRET=${CLIENT_SECRET} + - AUTH_URL=${AUTH_URL} + - TOKEN_URL=${TOKEN_URL} + - USERINFO_URL=${USERINFO_URL} + - DEBUG_MODE=${DEBUG_MODE} + - MOCK_OAUTH2=${MOCK_OAUTH2} + - DATABASE_USER=${DATABASE_USER} + - DATABASE_NAME=${DATABASE_NAME} + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_HOST=database + +volumes: + db_data: \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..5bd2306 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,694 @@ +// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" + + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "email": "rpailharey@grandlyon.com" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/admin/monthlyInfo": { + "get": { + "description": "Get details of all monthlyInfo", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyInfo" + ], + "summary": "List all monthlyInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MonthlyInfo" + } + } + } + } + }, + "put": { + "description": "Create/update a specific monthlyInfo' content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "monthlyInfo" + ], + "summary": "Create/update a specific monthlyInfo' content", + "parameters": [ + { + "description": "MonthlyInfo to create/update with new content", + "name": "monthlyInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.MonthlyInfo" + } + } + ], + "responses": { + "200": { + "description": "Updated successfully", + "schema": { + "$ref": "#/definitions/models.MonthlyInfo" + } + }, + "201": { + "description": "Created successfully", + "schema": { + "$ref": "#/definitions/models.MonthlyInfo" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/monthlyInfo/{year}/{month}": { + "get": { + "description": "Get details of a specific monthlyInfo", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyInfo" + ], + "summary": "Get details of a specific monthlyInfo", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyInfo", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyInfo", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MonthlyInfo" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Delete a specific monthlyInfo", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyInfo" + ], + "summary": "Delete a specific monthlyInfo", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyInfo", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyInfo", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successful delete", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/monthlyNews": { + "get": { + "description": "Get details of all monthlyNews", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyNews" + ], + "summary": "List all monthlyNews", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MonthlyNews" + } + } + } + } + }, + "put": { + "description": "Create/update a specific monthlyNews' content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "monthlyNews" + ], + "summary": "Create/update a specific monthlyNews' content", + "parameters": [ + { + "description": "MonthlyNews to create/update with new content", + "name": "monthlyNews", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.MonthlyNews" + } + } + ], + "responses": { + "200": { + "description": "Updated successfully", + "schema": { + "$ref": "#/definitions/models.MonthlyNews" + } + }, + "201": { + "description": "Created successfully", + "schema": { + "$ref": "#/definitions/models.MonthlyNews" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/monthlyNews/{year}/{month}": { + "get": { + "description": "Get details of a specific monthlyNews", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyNews" + ], + "summary": "Get details of a specific monthlyNews", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyNews", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyNews", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MonthlyNews" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Delete a specific monthlyNews", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyNews" + ], + "summary": "Delete a specific monthlyNews", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyNews", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyNews", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successful delete", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/poll": { + "get": { + "description": "Get details of all polls", + "produces": [ + "application/json" + ], + "tags": [ + "poll" + ], + "summary": "List all polls", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Poll" + } + } + } + } + }, + "put": { + "description": "Update a specific poll' content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "poll" + ], + "summary": "Update a specific poll' content", + "parameters": [ + { + "description": "Poll to update with new content", + "name": "poll", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Poll" + } + } + ], + "responses": { + "200": { + "description": "Updated successfully", + "schema": { + "$ref": "#/definitions/models.Poll" + } + }, + "201": { + "description": "Created successfully", + "schema": { + "$ref": "#/definitions/models.Poll" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/poll/{year}/{month}": { + "get": { + "description": "Get details of a specific poll", + "produces": [ + "application/json" + ], + "tags": [ + "poll" + ], + "summary": "Get details of a specific poll", + "parameters": [ + { + "type": "integer", + "description": "Year of the poll", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the poll", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Poll" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Delete a specific poll", + "produces": [ + "application/json" + ], + "tags": [ + "poll" + ], + "summary": "Delete a specific poll", + "parameters": [ + { + "type": "integer", + "description": "Year of the poll", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the poll", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successful delete", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/common/monthlyReport": { + "get": { + "description": "Find the MonthlyInfo of the current month and try to find the corresponding monthlyNews and poll", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyReport" + ], + "summary": "Get details of the current monthlyReport", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MonthlyReport" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/common/monthlyReport/{year}/{month}": { + "get": { + "description": "Get details of a specific monthlyReport", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyReport" + ], + "summary": "Get details of a specific monthlyReport", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyReport", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyReport", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MonthlyReport" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "models.MonthlyInfo": { + "type": "object", + "properties": { + "info": { + "type": "string" + }, + "month": { + "type": "integer" + }, + "year": { + "type": "integer" + } + } + }, + "models.MonthlyNews": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "month": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "year": { + "type": "integer" + } + } + }, + "models.MonthlyReport": { + "type": "object", + "properties": { + "info": { + "type": "string" + }, + "link": { + "type": "string" + }, + "month": { + "type": "integer" + }, + "newsContent": { + "type": "string" + }, + "newsTitle": { + "type": "string" + }, + "question": { + "type": "string" + }, + "year": { + "type": "integer" + } + } + }, + "models.Poll": { + "type": "object", + "properties": { + "link": { + "type": "string" + }, + "month": { + "type": "integer" + }, + "question": { + "type": "string" + }, + "year": { + "type": "integer" + } + } + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "1.0", + Host: "localhost:1443", + BasePath: "/", + Schemes: []string{}, + Title: "Backoffice API", + Description: "This is a sample service for managing newsletters for Ecolyo", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + "escape": func(v interface{}) string { + // escape tabs + str := strings.Replace(v.(string), "\t", "\\t", -1) + // replace " with \", and if that results in \\", replace that with \\\" + str = strings.Replace(str, "\"", "\\\"", -1) + return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register(swag.Name, &s{}) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..e7aa192 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,626 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample service for managing newsletters for Ecolyo", + "title": "Backoffice API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "email": "rpailharey@grandlyon.com" + }, + "version": "1.0" + }, + "host": "localhost:1443", + "basePath": "/", + "paths": { + "/api/admin/monthlyInfo": { + "get": { + "description": "Get details of all monthlyInfo", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyInfo" + ], + "summary": "List all monthlyInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MonthlyInfo" + } + } + } + } + }, + "put": { + "description": "Create/update a specific monthlyInfo' content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "monthlyInfo" + ], + "summary": "Create/update a specific monthlyInfo' content", + "parameters": [ + { + "description": "MonthlyInfo to create/update with new content", + "name": "monthlyInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.MonthlyInfo" + } + } + ], + "responses": { + "200": { + "description": "Updated successfully", + "schema": { + "$ref": "#/definitions/models.MonthlyInfo" + } + }, + "201": { + "description": "Created successfully", + "schema": { + "$ref": "#/definitions/models.MonthlyInfo" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/monthlyInfo/{year}/{month}": { + "get": { + "description": "Get details of a specific monthlyInfo", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyInfo" + ], + "summary": "Get details of a specific monthlyInfo", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyInfo", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyInfo", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MonthlyInfo" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Delete a specific monthlyInfo", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyInfo" + ], + "summary": "Delete a specific monthlyInfo", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyInfo", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyInfo", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successful delete", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/monthlyNews": { + "get": { + "description": "Get details of all monthlyNews", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyNews" + ], + "summary": "List all monthlyNews", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MonthlyNews" + } + } + } + } + }, + "put": { + "description": "Create/update a specific monthlyNews' content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "monthlyNews" + ], + "summary": "Create/update a specific monthlyNews' content", + "parameters": [ + { + "description": "MonthlyNews to create/update with new content", + "name": "monthlyNews", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.MonthlyNews" + } + } + ], + "responses": { + "200": { + "description": "Updated successfully", + "schema": { + "$ref": "#/definitions/models.MonthlyNews" + } + }, + "201": { + "description": "Created successfully", + "schema": { + "$ref": "#/definitions/models.MonthlyNews" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/monthlyNews/{year}/{month}": { + "get": { + "description": "Get details of a specific monthlyNews", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyNews" + ], + "summary": "Get details of a specific monthlyNews", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyNews", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyNews", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MonthlyNews" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Delete a specific monthlyNews", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyNews" + ], + "summary": "Delete a specific monthlyNews", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyNews", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyNews", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successful delete", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/poll": { + "get": { + "description": "Get details of all polls", + "produces": [ + "application/json" + ], + "tags": [ + "poll" + ], + "summary": "List all polls", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Poll" + } + } + } + } + }, + "put": { + "description": "Update a specific poll' content", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "poll" + ], + "summary": "Update a specific poll' content", + "parameters": [ + { + "description": "Poll to update with new content", + "name": "poll", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Poll" + } + } + ], + "responses": { + "200": { + "description": "Updated successfully", + "schema": { + "$ref": "#/definitions/models.Poll" + } + }, + "201": { + "description": "Created successfully", + "schema": { + "$ref": "#/definitions/models.Poll" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/admin/poll/{year}/{month}": { + "get": { + "description": "Get details of a specific poll", + "produces": [ + "application/json" + ], + "tags": [ + "poll" + ], + "summary": "Get details of a specific poll", + "parameters": [ + { + "type": "integer", + "description": "Year of the poll", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the poll", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Poll" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Delete a specific poll", + "produces": [ + "application/json" + ], + "tags": [ + "poll" + ], + "summary": "Delete a specific poll", + "parameters": [ + { + "type": "integer", + "description": "Year of the poll", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the poll", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "successful delete", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/common/monthlyReport": { + "get": { + "description": "Find the MonthlyInfo of the current month and try to find the corresponding monthlyNews and poll", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyReport" + ], + "summary": "Get details of the current monthlyReport", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MonthlyReport" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/common/monthlyReport/{year}/{month}": { + "get": { + "description": "Get details of a specific monthlyReport", + "produces": [ + "application/json" + ], + "tags": [ + "monthlyReport" + ], + "summary": "Get details of a specific monthlyReport", + "parameters": [ + { + "type": "integer", + "description": "Year of the monthlyReport", + "name": "year", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Month of the monthlyReport", + "name": "month", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.MonthlyReport" + } + }, + "404": { + "description": "Not found", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "models.MonthlyInfo": { + "type": "object", + "properties": { + "info": { + "type": "string" + }, + "month": { + "type": "integer" + }, + "year": { + "type": "integer" + } + } + }, + "models.MonthlyNews": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "month": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "year": { + "type": "integer" + } + } + }, + "models.MonthlyReport": { + "type": "object", + "properties": { + "info": { + "type": "string" + }, + "link": { + "type": "string" + }, + "month": { + "type": "integer" + }, + "newsContent": { + "type": "string" + }, + "newsTitle": { + "type": "string" + }, + "question": { + "type": "string" + }, + "year": { + "type": "integer" + } + } + }, + "models.Poll": { + "type": "object", + "properties": { + "link": { + "type": "string" + }, + "month": { + "type": "integer" + }, + "question": { + "type": "string" + }, + "year": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..5edbca6 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,416 @@ +basePath: / +definitions: + models.MonthlyInfo: + properties: + info: + type: string + month: + type: integer + year: + type: integer + type: object + models.MonthlyNews: + properties: + content: + type: string + month: + type: integer + title: + type: string + year: + type: integer + type: object + models.MonthlyReport: + properties: + info: + type: string + link: + type: string + month: + type: integer + newsContent: + type: string + newsTitle: + type: string + question: + type: string + year: + type: integer + type: object + models.Poll: + properties: + link: + type: string + month: + type: integer + question: + type: string + year: + type: integer + type: object +host: localhost:1443 +info: + contact: + email: rpailharey@grandlyon.com + name: API Support + description: This is a sample service for managing newsletters for Ecolyo + termsOfService: http://swagger.io/terms/ + title: Backoffice API + version: "1.0" +paths: + /api/admin/monthlyInfo: + get: + description: Get details of all monthlyInfo + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.MonthlyInfo' + type: array + summary: List all monthlyInfo + tags: + - monthlyInfo + put: + consumes: + - application/json + description: Create/update a specific monthlyInfo' content + parameters: + - description: MonthlyInfo to create/update with new content + in: body + name: monthlyInfo + required: true + schema: + $ref: '#/definitions/models.MonthlyInfo' + produces: + - application/json + responses: + "200": + description: Updated successfully + schema: + $ref: '#/definitions/models.MonthlyInfo' + "201": + description: Created successfully + schema: + $ref: '#/definitions/models.MonthlyInfo' + "400": + description: Bad Request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Create/update a specific monthlyInfo' content + tags: + - monthlyInfo + /api/admin/monthlyInfo/{year}/{month}: + delete: + description: Delete a specific monthlyInfo + parameters: + - description: Year of the monthlyInfo + in: path + name: year + required: true + type: integer + - description: Month of the monthlyInfo + in: path + name: month + required: true + type: integer + produces: + - application/json + responses: + "200": + description: successful delete + schema: + type: string + "404": + description: Not found + schema: + type: string + summary: Delete a specific monthlyInfo + tags: + - monthlyInfo + get: + description: Get details of a specific monthlyInfo + parameters: + - description: Year of the monthlyInfo + in: path + name: year + required: true + type: integer + - description: Month of the monthlyInfo + in: path + name: month + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.MonthlyInfo' + "404": + description: Not found + schema: + type: string + summary: Get details of a specific monthlyInfo + tags: + - monthlyInfo + /api/admin/monthlyNews: + get: + description: Get details of all monthlyNews + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.MonthlyNews' + type: array + summary: List all monthlyNews + tags: + - monthlyNews + put: + consumes: + - application/json + description: Create/update a specific monthlyNews' content + parameters: + - description: MonthlyNews to create/update with new content + in: body + name: monthlyNews + required: true + schema: + $ref: '#/definitions/models.MonthlyNews' + produces: + - application/json + responses: + "200": + description: Updated successfully + schema: + $ref: '#/definitions/models.MonthlyNews' + "201": + description: Created successfully + schema: + $ref: '#/definitions/models.MonthlyNews' + "400": + description: Bad Request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Create/update a specific monthlyNews' content + tags: + - monthlyNews + /api/admin/monthlyNews/{year}/{month}: + delete: + description: Delete a specific monthlyNews + parameters: + - description: Year of the monthlyNews + in: path + name: year + required: true + type: integer + - description: Month of the monthlyNews + in: path + name: month + required: true + type: integer + produces: + - application/json + responses: + "200": + description: successful delete + schema: + type: string + "404": + description: Not found + schema: + type: string + summary: Delete a specific monthlyNews + tags: + - monthlyNews + get: + description: Get details of a specific monthlyNews + parameters: + - description: Year of the monthlyNews + in: path + name: year + required: true + type: integer + - description: Month of the monthlyNews + in: path + name: month + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.MonthlyNews' + "404": + description: Not found + schema: + type: string + summary: Get details of a specific monthlyNews + tags: + - monthlyNews + /api/admin/poll: + get: + description: Get details of all polls + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Poll' + type: array + summary: List all polls + tags: + - poll + put: + consumes: + - application/json + description: Update a specific poll' content + parameters: + - description: Poll to update with new content + in: body + name: poll + required: true + schema: + $ref: '#/definitions/models.Poll' + produces: + - application/json + responses: + "200": + description: Updated successfully + schema: + $ref: '#/definitions/models.Poll' + "201": + description: Created successfully + schema: + $ref: '#/definitions/models.Poll' + "400": + description: Bad Request + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Update a specific poll' content + tags: + - poll + /api/admin/poll/{year}/{month}: + delete: + description: Delete a specific poll + parameters: + - description: Year of the poll + in: path + name: year + required: true + type: integer + - description: Month of the poll + in: path + name: month + required: true + type: integer + produces: + - application/json + responses: + "200": + description: successful delete + schema: + type: string + "404": + description: Not found + schema: + type: string + summary: Delete a specific poll + tags: + - poll + get: + description: Get details of a specific poll + parameters: + - description: Year of the poll + in: path + name: year + required: true + type: integer + - description: Month of the poll + in: path + name: month + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Poll' + "404": + description: Not found + schema: + type: string + summary: Get details of a specific poll + tags: + - poll + /api/common/monthlyReport: + get: + description: Find the MonthlyInfo of the current month and try to find the corresponding + monthlyNews and poll + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.MonthlyReport' + "404": + description: Not found + schema: + type: string + summary: Get details of the current monthlyReport + tags: + - monthlyReport + /api/common/monthlyReport/{year}/{month}: + get: + description: Get details of a specific monthlyReport + parameters: + - description: Year of the monthlyReport + in: path + name: year + required: true + type: integer + - description: Month of the monthlyReport + in: path + name: month + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.MonthlyReport' + "404": + description: Not found + schema: + type: string + summary: Get details of a specific monthlyReport + tags: + - monthlyReport +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..985e21b --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server + +go 1.15 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/swaggo/http-swagger v1.1.1 + github.com/swaggo/swag v1.7.1 + golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f + gorm.io/driver/mysql v1.1.2 + gorm.io/driver/sqlite v1.1.4 + gorm.io/gorm v1.21.14 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..91f942d --- /dev/null +++ b/go.sum @@ -0,0 +1,465 @@ +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/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= +github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= +github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/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 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/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 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +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/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/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= +github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= +github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= +github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= +github.com/swaggo/http-swagger v1.1.1 h1:7cBYOcF/TS0Nx5uA6oOP9DfFV5RYogpazzK1IUmQUII= +github.com/swaggo/http-swagger v1.1.1/go.mod h1:cKIcshBU9yEAnfWv6ZzVKSsEf8h5ozxB8/zHQWyOQ/8= +github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= +github.com/swaggo/swag v1.7.1 h1:gY9ZakXlNWg/i/v5bQBic7VMZ4teq4m89lpiao74p/s= +github.com/swaggo/swag v1.7.1/go.mod h1:gAiHxNTb9cIpNmA/VEGUP+CyZMCP/EW7mdtc8Bny+p8= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +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= +github.com/yuin/goldmark v1.2.1/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= +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/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 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +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-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-20190827160401-ba9fcec4b297/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-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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201207224615-747e23833adb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/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/sync v0.0.0-20201020160332-67f06af15bc9/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-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-20191001151750-bb3f8db39f24/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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/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/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208062317-e652b2f42cc7/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M= +gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM= +gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= +gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= +gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.21.14 h1:NAR9A/3SoyiPVHouW/rlpMUZvuQZ6Z6UYGz+2tosSQo= +gorm.io/gorm v1.21.14/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +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= +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/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..fd469bd --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,215 @@ +package auth + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "os" + "strings" + "time" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/tokens" +) + +type key int + +const ( + authTokenKey string = "auth_token" + // ContextData is the user + ContextData key = 0 +) + +var ( + // AdminRole represents the role reserved for admins + AdminRole = common.StringValueFromEnv("ADMIN_ROLE", "ADMINS") + hostname = common.StringValueFromEnv("HOSTNAME", "ecolyobackoffice.127.0.0.1.nip.io") +) + +// User represents a logged in user +type User struct { + ID string `json:"id,omitempty"` + Login string `json:"login"` + DisplayName string `json:"displayName,omitempty"` + Email string `json:"email,omitempty"` + Roles []string `json:"memberOf"` + IsAdmin bool `json:"isAdmin,omitempty"` + Name string `json:"name,omitempty"` + Surname string `json:"surname,omitempty"` + PasswordHash string `json:"passwordHash,omitempty"` + Password string `json:"password,omitempty"` +} + +// TokenData represents the data held into a token +type TokenData struct { + User + URL string `json:"url,omitempty"` + ReadOnly bool `json:"readonly,omitempty"` + SharingUserLogin string `json:"sharinguserlogin,omitempty"` + XSRFToken string `json:"xsrftoken,omitempty"` +} + +func AdminAuthMiddleware(next http.Handler) http.Handler { + return ValidateAuthMiddleware(next, []string{os.Getenv("ADMIN_ROLE")}, true) +} + +func CommonAuthMiddleware(next http.Handler) http.Handler { + return ValidateAuthMiddleware(next, []string{"*"}, false) +} + +// ValidateAuthMiddleware validates that the token is valid and that the user has the correct roles +func ValidateAuthMiddleware(next http.Handler, allowedRoles []string, checkXSRF bool) http.Handler { + roleChecker := func(w http.ResponseWriter, r *http.Request) { + user := TokenData{} + checkXSRF, err := tokens.ExtractAndValidateToken(r, authTokenKey, &user, checkXSRF) + if err != nil { + // Handle CORS preflight requests + if r.Method == "OPTIONS" { + return + } + // Default to redirect to authentication + redirectTo := hostname + _, port, perr := net.SplitHostPort(r.Host) + if perr == nil { + redirectTo += ":" + port + } + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusUnauthorized) + responseContent := fmt.Sprintf("error extracting token: %v<meta http-equiv=\"Refresh\" content=\"0; url=https://%v#login\"/>", err.Error(), redirectTo) + fmt.Fprint(w, responseContent) + return + + } + // Check XSRF Token + if checkXSRF && r.Header.Get("XSRF-TOKEN") != user.XSRFToken { + http.Error(w, "XSRF protection triggered", http.StatusUnauthorized) + return + } + err = checkUserHasRole(user, allowedRoles) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + err = checkUserHasRole(user, []string{AdminRole}) + if err == nil { + user.IsAdmin = true + } + // Check for url + if user.URL != "" { + requestURL := strings.Split(r.Host, ":")[0] + r.URL.EscapedPath() + if user.URL != requestURL { + http.Error(w, "token restricted to url: "+user.URL, http.StatusUnauthorized) + return + } + } + // Check for method + if user.ReadOnly && r.Method != http.MethodGet { + http.Error(w, "token is read only", http.StatusForbidden) + return + } + ctx := context.WithValue(r.Context(), ContextData, user) + next.ServeHTTP(w, r.WithContext(ctx)) + } + return http.HandlerFunc(roleChecker) +} + +// HandleLogout remove the user from the cookie store +func (m Manager) HandleLogout(w http.ResponseWriter, r *http.Request) { + // Delete the auth cookie + c := http.Cookie{ + Name: authTokenKey, + Domain: m.Hostname, + MaxAge: -1, + } + http.SetCookie(w, &c) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} + +// WhoAmI returns the user data +func WhoAmI() http.Handler { + whoAmI := func(w http.ResponseWriter, r *http.Request) { + user, err := GetTokenData(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + json.NewEncoder(w).Encode(user) + } + return http.HandlerFunc(whoAmI) +} + +// checkUserHasRole checks if the user has the required role +func checkUserHasRole(user TokenData, allowedRoles []string) error { + for _, allowedRole := range allowedRoles { + if allowedRole == "*" { + return nil + } + for _, userRole := range user.Roles { + if userRole != "" && (userRole == allowedRole) { + return nil + } + } + } + return fmt.Errorf("no user role among %v is in allowed roles (%v)", user.Roles, allowedRoles) +} + +//GetShareToken gets a share token for a given ressource +func GetShareToken(w http.ResponseWriter, r *http.Request) { + user, err := GetTokenData(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var wantedToken struct { + Sharedfor string `json:"sharedfor"` + URL string `json:"url"` + Lifespan int `json:"lifespan"` + ReadOnly bool `json:"readonly,omitempty"` + } + err = json.NewDecoder(r.Body).Decode(&wantedToken) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if wantedToken.URL == "" { + http.Error(w, "url cannot be empty", http.StatusBadRequest) + return + } + user.Login = user.Login + "_share_for_" + wantedToken.Sharedfor + user.URL = wantedToken.URL + user.ReadOnly = wantedToken.ReadOnly + user.SharingUserLogin = wantedToken.Sharedfor + token, err := tokens.CreateToken(user, time.Now().Add(time.Hour*time.Duration(24*wantedToken.Lifespan))) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fmt.Fprint(w, token) +} + +// GetTokenData gets an user from a request +func GetTokenData(r *http.Request) (TokenData, error) { + user, ok := r.Context().Value(ContextData).(TokenData) + if !ok { + return user, errors.New("user could not be got from context") + } + return user, nil +} + +// isWebdav works out if an user agent is a webdav user agent +func isWebdav(ua string) bool { + for _, a := range []string{"vfs", "Microsoft-WebDAV", "Konqueror", "LibreOffice", "Rei.Fs.WebDAV"} { + if strings.Contains(ua, a) { + return true + } + } + return false +} diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go new file mode 100644 index 0000000..870b2de --- /dev/null +++ b/internal/auth/oauth2.go @@ -0,0 +1,143 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/tokens" + "golang.org/x/oauth2" +) + +const ( + oAuth2StateKey string = "oauth2_state" +) + +// Manager exposes the handlers for OAuth2 endpoints +type Manager struct { + Config *oauth2.Config + Hostname string + UserInfoURL string +} + +// NewManager returns a new Manager according to environment variables +func NewManager() Manager { + return Manager{Config: &oauth2.Config{ + RedirectURL: os.Getenv("REDIRECT_URL"), + ClientID: os.Getenv("CLIENT_ID"), + ClientSecret: os.Getenv("CLIENT_SECRET"), + Scopes: []string{"login", "memberOf", "displayName", "email"}, + Endpoint: oauth2.Endpoint{ + AuthURL: os.Getenv("AUTH_URL"), + TokenURL: os.Getenv("TOKEN_URL"), + }, + }, + Hostname: common.StringValueFromEnv("HOSTNAME", "ecolyobackoffice.127.0.0.1.nip.io"), + UserInfoURL: os.Getenv("USERINFO_URL"), + } +} + +// HandleOAuth2Login handles the OAuth2 login +func (m Manager) HandleOAuth2Login(w http.ResponseWriter, r *http.Request) { + // Generate state and store it in cookie + oauthStateString, err := common.GenerateRandomString(16) + if err != nil { + log.Fatalf("Error generating OAuth2 strate string :%v\n", err) + } + tokens.CreateCookie(oauthStateString, m.Hostname, oAuth2StateKey, 60*time.Second, w) + url := m.Config.AuthCodeURL(oauthStateString) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +// HandleOAuth2Callback handles the OAuth2 Callback and get user info +func (m Manager) HandleOAuth2Callback() http.Handler { + oauth2Handler := func(w http.ResponseWriter, r *http.Request) { + // Recover state from tokens + var oauthState string + _, err := tokens.ExtractAndValidateToken(r, oAuth2StateKey, &oauthState, false) + if err != nil { + fmt.Println("Code exchange failed with ", err) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + // Check states match + state := r.FormValue("state") + if state != oauthState { + fmt.Printf("invalid oauth state, expected '%s', got '%s'\n", oauthState, state) + http.Error(w, "invalid oauth state", http.StatusInternalServerError) + return + } + // Delete the state cookie + c := http.Cookie{ + Name: oAuth2StateKey, + Domain: m.Hostname, + MaxAge: -1, + } + http.SetCookie(w, &c) + // Perform code exchange + code := r.FormValue("code") + token, err := m.Config.Exchange(context.Background(), code) + if err != nil { + fmt.Printf("Code exchange failed with '%s'\n", err) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + // Get user infos + client := &http.Client{} + req, _ := http.NewRequest("GET", m.UserInfoURL+"?access_token="+token.AccessToken, nil) + req.Header.Set("Authorization", "Bearer "+token.AccessToken) + response, err := client.Do(req) + if err != nil || response.StatusCode == http.StatusBadRequest { + fmt.Printf("User info failed with '%s'\n", err) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + // Get user + var user User + if response.Body == nil { + http.Error(w, "no response body", http.StatusBadRequest) + return + } + //////////////////////////////////////////////// + // UNCOMMENT THIS TO DEBUG USERINFO RESPONSE // + // readBody, err := ioutil.ReadAll(response.Body) + // if err != nil { + // panic(err) + // } + // newBody := ioutil.NopCloser(bytes.NewBuffer(readBody)) + // response.Body = newBody + // if string(readBody) != "" { + // fmt.Printf("BODY : %q \n", readBody) + // } + //////////////////////////////////////////////// + err = json.NewDecoder(response.Body).Decode(&user) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Trim the user roles in case they come from LDAP + for key, role := range user.Roles { + user.Roles[key] = strings.TrimPrefix(strings.Split(role, ",")[0], "CN=") + } + // Store the user in cookie + // Generate + xsrfToken, err := common.GenerateRandomString(16) + if err != nil { + http.Error(w, "error generating XSRF Token", http.StatusInternalServerError) + return + } + tokenData := TokenData{User: user, XSRFToken: xsrfToken} + tokens.CreateCookie(tokenData, m.Hostname, authTokenKey, 24*time.Hour, w) + // Log the connexion + log.Printf("| %v (%v %v) | Login success | %v", user.Login, user.Name, user.Surname, req.RemoteAddr) + // Redirect + http.Redirect(w, r, "/", http.StatusFound) + } + return http.HandlerFunc(oauth2Handler) +} diff --git a/internal/common/common.go b/internal/common/common.go new file mode 100644 index 0000000..708ba1c --- /dev/null +++ b/internal/common/common.go @@ -0,0 +1,173 @@ +package common + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "io" + "net/http" + "os" + "path" + "strconv" + "sync" + + "log" + + "github.com/gorilla/mux" +) + +var ( + disableLogFatal = false + lock sync.Mutex // Mutex used to lock file writing +) + +// Save saves a representation of v to the file at path. +func Save(path string, v interface{}) error { + lock.Lock() + defer lock.Unlock() + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + r, err := Marshal(v) + if err != nil { + return err + } + _, err = io.Copy(f, r) + return err +} + +// Load loads the file at path into v. Use os.IsNotExist() to see if the returned error is due to the file being missing. +func Load(path string, v interface{}) error { + lock.Lock() + defer lock.Unlock() + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + return Unmarshal(f, v) +} + +// Marshal is a function that marshals the object into an io.Reader. By default, it uses the JSON marshaller. +var Marshal = func(v interface{}) (io.Reader, error) { + b, err := json.MarshalIndent(v, "", "\t") + if err != nil { + return nil, err + } + return bytes.NewReader(b), nil +} + +// Unmarshal is a function that unmarshals the data from the reader into the specified value. By default, it uses the JSON unmarshaller. +var Unmarshal = func(r io.Reader, v interface{}) error { + return json.NewDecoder(r).Decode(v) +} + +// GenerateRandomBytes returns securely generated random bytes. +// It will return an error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + return b, nil +} + +// GenerateRandomString returns a URL-safe, base64 encoded +// securely generated random string. +// It will return an error if the system's secure random +// number generator fails to function correctly, in which +// case the caller should not continue. +func GenerateRandomString(s int) (string, error) { + b, err := GenerateRandomBytes(s) + return base64.URLEncoding.EncodeToString(b), err +} + +// FallBackWrapper serves a file if found and else default to index.html +type FallBackWrapper struct { + Assets http.FileSystem +} + +// Open serves a file if found and else default to index.html +func (i *FallBackWrapper) Open(name string) (http.File, error) { + file, err := i.Assets.Open(name) + // If the file is found but there is another error or the asked for file has an extension : return the file or error + if !os.IsNotExist(err) || path.Ext(name) != "" { + return file, err + } + // Else fall back to index.html + return i.Assets.Open("index.html") +} + +// Contains works out if a string slice contains a given string element +func Contains(a []string, x string) bool { + for _, n := range a { + if x == n { + return true + } + } + return false +} + +// StringValueFromEnv set a value into an *interface from an environment variable or default +func StringValueFromEnv(ev string, def string) string { + val := os.Getenv(ev) + if val == "" { + return def + } + return val +} + +// IntValueFromEnv set a value into an *interface from an environment variable or default +func IntValueFromEnv(ev string, def int) int { + val := os.Getenv(ev) + if val == "" { + return def + } + v, err := strconv.Atoi(val) + if err != nil && !disableLogFatal { + log.Fatalf("Error : could not get integer value from environment variable %v=%v\n", ev, val) + } + return v +} + +// BoolValueFromEnv set a value into an *interface from an environment variable or default +func BoolValueFromEnv(ev string, def bool) bool { + val := os.Getenv(ev) + if val == "" { + return def + } + v, err := strconv.ParseBool(val) + if err != nil && !disableLogFatal { + log.Fatalf("Error : could not get boolean value from environment variable %v=%v\n", ev, val) + } + return v +} + +func YearMonthFromRequest(r *http.Request) (year int, month int, err error) { + vars := mux.Vars(r) + yearStr := vars["year"] + monthStr := vars["month"] + + if yearStr == "" || monthStr == "" { + return 0, 0, errors.New("missing query element") + } + + year, err = strconv.Atoi(yearStr) + if err != nil { + return 0, 0, errors.New("year is not an integer") + } + month, err = strconv.Atoi(monthStr) + if err != nil { + return 0, 0, errors.New("month is not an integer") + } + + return year, month, nil +} diff --git a/internal/common/common_test.go b/internal/common/common_test.go new file mode 100644 index 0000000..e8e5fbd --- /dev/null +++ b/internal/common/common_test.go @@ -0,0 +1,86 @@ +package common + +import ( + "os" + "testing" +) + +func init() { + disableLogFatal = true +} + +func TestStringValueFromEnv(t *testing.T) { + os.Setenv("MY_EV", "from_env") + var rv string + type args struct { + ev string + def string + } + tests := []struct { + name string + args args + expected string + }{ + {"string_value_from_env", args{"MY_EV", "test"}, "from_env"}, + {"string_value_from_def", args{"MY_DEF", "test"}, "test"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rv = StringValueFromEnv(tt.args.ev, tt.args.def) + if rv != tt.expected { + t.Errorf("StringValueFromEnv() error ; got %v, expected %v", rv, tt.expected) + } + }) + } +} + +func TestIntValueFromEnv(t *testing.T) { + os.Setenv("MY_EV", "from_env") + var rv int + type args struct { + ev string + def int + } + tests := []struct { + name string + args args + expected int + }{ + {"int_value_from_def", args{"MY_DEF", 1}, 1}, + {"string_on_int_from_env", args{"MY_EV", 1}, 0}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rv = IntValueFromEnv(tt.args.ev, tt.args.def) + if rv != tt.expected { + t.Errorf("IntValueFromEnv() error ; got %v, expected %v", rv, tt.expected) + } + }) + } +} + +func TestBoolValueFromEnv(t *testing.T) { + os.Setenv("MY_EV", "from_env") + var rv bool + type args struct { + ev string + def bool + } + tests := []struct { + name string + args args + expected bool + }{ + + {"bool_value_from_def", args{"MY_DEF", true}, true}, + {"string_on_bool_from_def", args{"MY_EV", true}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rv = BoolValueFromEnv(tt.args.ev, tt.args.def) + if rv != tt.expected { + t.Errorf("BoolValueFromEnv() error ; got %v, expected %v", rv, tt.expected) + } + }) + } +} diff --git a/internal/file/file.go b/internal/file/file.go new file mode 100644 index 0000000..dba702c --- /dev/null +++ b/internal/file/file.go @@ -0,0 +1,38 @@ +package file + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" +) + +var imageFolder = common.StringValueFromEnv("IMAGE_FOLDER", "") + +func GetEcogestureImages(w http.ResponseWriter, r *http.Request) { + filenames, err := fileNamesFromFolder(imageFolder) + jsondata, err := json.Marshal(filenames) + if err != nil { + fmt.Printf("Error: %s", err.Error()) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(jsondata) + log.Printf("| get image names | %v", r.RemoteAddr) +} + +func fileNamesFromFolder(folder string) (filenames []string, err error) { + files, err := ioutil.ReadDir(folder) + if err != nil { + return nil, err + } + + for _, file := range files { + filenames = append(filenames, file.Name()) + } + return filenames, nil +} diff --git a/internal/file/file_test.go b/internal/file/file_test.go new file mode 100644 index 0000000..d8dcc99 --- /dev/null +++ b/internal/file/file_test.go @@ -0,0 +1,38 @@ +package file + +import ( + "encoding/json" + "log" + "os" + "testing" +) + +func TestFilenamesFromFolder(t *testing.T) { + + // Create temporary file + err := os.MkdirAll("test", 0755) + if err != nil { + log.Fatal(err) + } + // Create some files + os.Create("test/file1") + os.Create("test/file2") + os.Create("test/file3") + expected := `["file1","file2","file3"]` + + filenames, err := fileNamesFromFolder("test") + if err != nil { + t.Errorf(`error: %s`, err) + return + } + jsondata, err := json.Marshal(filenames) + if err != nil { + t.Errorf(`error: %s`, err) + return + } + if string(jsondata) != expected { + t.Errorf(`unexpected answer: got %s want %s`, string(jsondata), expected) + return + } + os.RemoveAll("test") +} diff --git a/internal/mocks/mocks.go b/internal/mocks/mocks.go new file mode 100644 index 0000000..f25b70c --- /dev/null +++ b/internal/mocks/mocks.go @@ -0,0 +1,71 @@ +// Package mocks provide mocks for development purposes (debug mode) +package mocks + +import ( + "fmt" + "net/http" +) + +var ( + port int +) + +// Init initialize the configuration +func Init(portFromMain int) { + port = portFromMain +} + +// CreateMockOAuth2 creates a mock OAuth2 serve mux for development purposes +func CreateMockOAuth2() *http.ServeMux { + mux := http.NewServeMux() + // Returns authorization code back to the user + mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + redir := query.Get("redirect_uri") + "?state=" + query.Get("state") + "&code=mock_code" + http.Redirect(w, r, redir, http.StatusFound) + }) + // Returns authorization code back to the user, but without the provided state + mux.HandleFunc("/auth-wrong-state", func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + redir := query.Get("redirect_uri") + "?state=" + "a-random-state" + "&code=mock_code" + http.Redirect(w, r, redir, http.StatusFound) + }) + + // Returns access token back to the user + mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + w.Write([]byte("access_token=mocktoken&scope=user&token_type=bearer")) + }) + // Returns userinfo back to the user + mux.HandleFunc("/userinfo", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "displayName": "Us ER", + "memberOf": [ + "CN=USERS", + "CN=OTHER_GROUP" + ], + "id": "1000", + "login": "USER" + }`)) + }) + // Returns userinfo back to the user (with an admin user) + mux.HandleFunc("/admininfo", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "displayName": "Ad MIN", + "memberOf": [ + "CN=ADMINS", + "CN=OTHER_GROUP" + ], + "id": "1", + "login": "ADMIN" + }`)) + }) + // Logout + mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Logout OK") + }) + + return mux +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..fc72cf8 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,45 @@ +package models + +import ( + "fmt" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" + "gorm.io/driver/mysql" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type DataHandler struct { + db *gorm.DB +} + +var ( + dbUser = common.StringValueFromEnv("DATABASE_USER", "") + dbPassword = common.StringValueFromEnv("DATABASE_PASSWORD", "") + dbName = common.StringValueFromEnv("DATABASE_NAME", "") + dbHost = common.StringValueFromEnv("DATABASE_HOST", "") +) + +// NewDataHandler init a DataHandler and returns a pointer to it +func NewDataHandler() *DataHandler { + var db *gorm.DB + var err error + if dbUser == "" || dbPassword == "" || dbName == "" { + db, err = gorm.Open(sqlite.Open("backoffice.db"), &gorm.Config{}) + if err != nil { + panic("failed to connect database") + } + } else { + dsn := fmt.Sprintf("%v:%v@tcp(%v:3306)/%v?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPassword, dbHost, dbName) + db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + panic("failed to connect database") + } + } + + // Migrate the schema + db.AutoMigrate(&MonthlyInfo{}) + db.AutoMigrate(&MonthlyNews{}) + db.AutoMigrate(&Poll{}) + return &DataHandler{db: db} +} diff --git a/internal/models/monthlyInfo.go b/internal/models/monthlyInfo.go new file mode 100644 index 0000000..5425f8b --- /dev/null +++ b/internal/models/monthlyInfo.go @@ -0,0 +1,155 @@ +package models + +import ( + "encoding/json" + "log" + "net/http" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" +) + +type MonthlyInfo struct { + Year int `json:"year"` + Month int `json:"month"` + Info string `json:"info"` + Image string `json:"image"` +} + +// GetAllMonthlyInfo godoc +// @Summary List all monthlyInfo +// @Description Get details of all monthlyInfo +// @Tags monthlyInfo +// @Produce json +// @Success 200 {array} MonthlyInfo +// @Router /api/admin/monthlyInfo [get] +func (dh *DataHandler) GetAllMonthlyInfo(w http.ResponseWriter, r *http.Request) { + var monthlyInfo []MonthlyInfo + dh.db.Find(&monthlyInfo) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(monthlyInfo) + log.Printf("| get all monthlyInfo | %v", r.RemoteAddr) +} + +// GetSingleMonthlyInfo godoc +// @Summary Get details of a specific monthlyInfo +// @Description Get details of a specific monthlyInfo +// @Tags monthlyInfo +// @Produce json +// @Success 200 {object} MonthlyInfo +// @Failure 404 {string} string "Not found" +// @Param year path int true "Year of the monthlyInfo" +// @Param month path int true "Month of the monthlyInfo" +// @Router /api/admin/monthlyInfo/{year}/{month} [get] +func (dh *DataHandler) GetSingleMonthlyInfo(w http.ResponseWriter, r *http.Request) { + year, month, err := common.YearMonthFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + + var monthlyInfo MonthlyInfo + err = dh.db.Where("year = ? AND month = ?", year, month).First(&monthlyInfo).Error + if err != nil { + // If not found, answer "not found" + w.WriteHeader(http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(monthlyInfo) +} + +// SaveMonthlyInfo godoc +// @Summary Create/update a specific monthlyInfo' content +// @Description Create/update a specific monthlyInfo' content +// @Tags monthlyInfo +// @Accept json +// @Produce json +// @Success 200 {object} MonthlyInfo "Updated successfully" +// @Success 201 {object} MonthlyInfo "Created successfully" +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal server error" +// @Param monthlyInfo body MonthlyInfo true "MonthlyInfo to create/update with new content" +// @Router /api/admin/monthlyInfo [put] +func (dh *DataHandler) SaveMonthlyInfo(w http.ResponseWriter, r *http.Request) { + if r.Body == http.NoBody { + http.Error(w, "request body is empty", http.StatusBadRequest) + return + } + + decoder := json.NewDecoder(r.Body) + var monthlyInfo MonthlyInfo + err := decoder.Decode(&monthlyInfo) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Check if this monthlyInfo exists + err = dh.db.Where("year = ? AND month = ?", monthlyInfo.Year, monthlyInfo.Month).First(&MonthlyInfo{}).Error + + if err != nil { + // Create a monthlyInfo + err = dh.db.Create(&MonthlyInfo{ + Year: monthlyInfo.Year, + Month: monthlyInfo.Month, + Info: monthlyInfo.Info, + Image: monthlyInfo.Image, + }).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(monthlyInfo) + + log.Printf("| new monthlyInfo | year : %d month : %d | %v", monthlyInfo.Year, monthlyInfo.Month, r.RemoteAddr) + return + + } else { + // Update info + err = dh.db.Model(&MonthlyInfo{}).Where("year = ? AND month = ?", monthlyInfo.Year, monthlyInfo.Month).Update("info", monthlyInfo.Info).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Update info + err = dh.db.Model(&MonthlyInfo{}).Where("year = ? AND month = ?", monthlyInfo.Year, monthlyInfo.Month).Update("image", monthlyInfo.Image).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(monthlyInfo) + log.Printf("| updated monthlyInfo | year : %d month : %d | %v", monthlyInfo.Year, monthlyInfo.Month, r.RemoteAddr) + } +} + +// DeleteMonthlyInfo godoc +// @Summary Delete a specific monthlyInfo +// @Description Delete a specific monthlyInfo +// @Tags monthlyInfo +// @Produce json +// @Success 200 {string} string "successful delete" +// @Failure 404 {string} string "Not found" +// @Param year path int true "Year of the monthlyInfo" +// @Param month path int true "Month of the monthlyInfo" +// @Router /api/admin/monthlyInfo/{year}/{month} [delete] +func (dh *DataHandler) DeleteMonthlyInfo(w http.ResponseWriter, r *http.Request) { + year, month, err := common.YearMonthFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = dh.db.Where("year = ? AND month = ?", year, month).Delete(&MonthlyInfo{}).Error + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("successful delete")) +} diff --git a/internal/models/monthlyNews.go b/internal/models/monthlyNews.go new file mode 100644 index 0000000..5a7ce8e --- /dev/null +++ b/internal/models/monthlyNews.go @@ -0,0 +1,155 @@ +package models + +import ( + "encoding/json" + "log" + "net/http" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" +) + +type MonthlyNews struct { + Year int `json:"year"` + Month int `json:"month"` + Title string `json:"title"` + Content string `json:"content"` +} + +// GetAllMonthlyNews godoc +// @Summary List all monthlyNews +// @Description Get details of all monthlyNews +// @Tags monthlyNews +// @Produce json +// @Success 200 {array} MonthlyNews +// @Router /api/admin/monthlyNews [get] +func (dh *DataHandler) GetAllMonthlyNews(w http.ResponseWriter, r *http.Request) { + var monthlyNews []MonthlyNews + dh.db.Find(&monthlyNews) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(monthlyNews) + log.Printf("| get all monthlyNews | %v", r.RemoteAddr) +} + +// GetSingleMonthlyNews godoc +// @Summary Get details of a specific monthlyNews +// @Description Get details of a specific monthlyNews +// @Tags monthlyNews +// @Produce json +// @Success 200 {object} MonthlyNews +// @Failure 404 {string} string "Not found" +// @Param year path int true "Year of the monthlyNews" +// @Param month path int true "Month of the monthlyNews" +// @Router /api/admin/monthlyNews/{year}/{month} [get] +func (dh *DataHandler) GetSingleMonthlyNews(w http.ResponseWriter, r *http.Request) { + year, month, err := common.YearMonthFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + + var monthlyNews MonthlyNews + err = dh.db.Where("year = ? AND month = ?", year, month).First(&monthlyNews).Error + if err != nil { + // If not found, answer "not found" + w.WriteHeader(http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(monthlyNews) +} + +// SaveMonthlyNews godoc +// @Summary Create/update a specific monthlyNews' content +// @Description Create/update a specific monthlyNews' content +// @Tags monthlyNews +// @Accept json +// @Produce json +// @Success 200 {object} MonthlyNews "Updated successfully" +// @Success 201 {object} MonthlyNews "Created successfully" +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal server error" +// @Param monthlyNews body MonthlyNews true "MonthlyNews to create/update with new content" +// @Router /api/admin/monthlyNews [put] +func (dh *DataHandler) SaveMonthlyNews(w http.ResponseWriter, r *http.Request) { + if r.Body == http.NoBody { + http.Error(w, "request body is empty", http.StatusBadRequest) + return + } + + decoder := json.NewDecoder(r.Body) + var monthlyNews MonthlyNews + err := decoder.Decode(&monthlyNews) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Check if this monthlyNews exists + err = dh.db.Where("year = ? AND month = ?", monthlyNews.Year, monthlyNews.Month).First(&MonthlyNews{}).Error + + // Default title + if monthlyNews.Title == "" { + monthlyNews.Title = "Les nouveautés du service" + } + + if err != nil { + + // Create a monthlyNews + err = dh.db.Create(&MonthlyNews{Year: monthlyNews.Year, Month: monthlyNews.Month, Title: monthlyNews.Title, Content: monthlyNews.Content}).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(monthlyNews) + + log.Printf("| new monthlyNews | year : %d month : %d title : %v| %v", monthlyNews.Year, monthlyNews.Month, monthlyNews.Title, r.RemoteAddr) + return + + } else { + // Update title + err = dh.db.Model(&MonthlyNews{}).Where("year = ? AND month = ?", monthlyNews.Year, monthlyNews.Month).Update("title", monthlyNews.Title).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + // Update content + err = dh.db.Model(&MonthlyNews{}).Where("year = ? AND month = ?", monthlyNews.Year, monthlyNews.Month).Update("content", monthlyNews.Content).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(monthlyNews) + log.Printf("| updated monthlyNews | year : %d month : %d title : %v| %v", monthlyNews.Year, monthlyNews.Month, monthlyNews.Title, r.RemoteAddr) + } +} + +// DeleteMonthlyNews godoc +// @Summary Delete a specific monthlyNews +// @Description Delete a specific monthlyNews +// @Tags monthlyNews +// @Produce json +// @Success 200 {string} string "successful delete" +// @Failure 404 {string} string "Not found" +// @Param year path int true "Year of the monthlyNews" +// @Param month path int true "Month of the monthlyNews" +// @Router /api/admin/monthlyNews/{year}/{month} [delete] +func (dh *DataHandler) DeleteMonthlyNews(w http.ResponseWriter, r *http.Request) { + year, month, err := common.YearMonthFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = dh.db.Where("year = ? AND month = ?", year, month).Delete(&MonthlyNews{}).Error + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("successful delete")) +} diff --git a/internal/models/monthlyReport.go b/internal/models/monthlyReport.go new file mode 100644 index 0000000..eb8816b --- /dev/null +++ b/internal/models/monthlyReport.go @@ -0,0 +1,96 @@ +package models + +import ( + "encoding/json" + "net/http" + "time" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" +) + +type MonthlyReport struct { + Year int `json:"year"` + Month int `json:"month"` + Info string `json:"info"` + Image string `json:"image"` + NewsTitle string `json:"newsTitle"` + NewsContent string `json:"newsContent"` + Question string `json:"question"` + Link string `json:"link"` +} + +// GetMonthlyReport godoc +// @Summary Get details of a specific monthlyReport +// @Description Get details of a specific monthlyReport +// @Tags monthlyReport +// @Produce json +// @Success 200 {object} MonthlyReport +// @Failure 404 {string} string "Not found" +// @Param year path int true "Year of the monthlyReport" +// @Param month path int true "Month of the monthlyReport" +// @Router /api/common/monthlyReport/{year}/{month} [get] +func (dh *DataHandler) GetMonthlyReport(w http.ResponseWriter, r *http.Request) { + year, month, err := common.YearMonthFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + monthlyReport, err := dh.getMonthlyReport(year, month) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(monthlyReport) +} + +// GetCurrentMonthlyReport godoc +// @Summary Get details of the current monthlyReport +// @Description Find the MonthlyInfo of the current month and try to find the corresponding monthlyNews and poll +// @Tags monthlyReport +// @Produce json +// @Success 200 {object} MonthlyReport +// @Failure 404 {string} string "Not found" +// @Router /api/common/monthlyReport [get] +func (dh *DataHandler) GetCurrentMonthlyReport(w http.ResponseWriter, r *http.Request) { + + year, month, _ := time.Now().Date() + + monthlyReport, err := dh.getMonthlyReport(year, int(month)-1) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(monthlyReport) +} + +func (dh *DataHandler) getMonthlyReport(year int, month int) (monthlyReport MonthlyReport, err error) { + var monthlyInfo MonthlyInfo + err = dh.db.Where("year = ? AND month = ?", year, month).First(&monthlyInfo).Error + if err != nil { + return MonthlyReport{}, err + } + + var monthlyNews MonthlyNews + dh.db.Where("year = ? AND month = ?", year, month).First(&monthlyNews) + + var poll Poll + dh.db.Where("year = ? AND month = ?", year, month).First(&poll) + + monthlyReport = MonthlyReport{ + Year: year, + Month: month, + Info: monthlyInfo.Info, + Image: monthlyInfo.Image, + NewsTitle: monthlyNews.Title, + NewsContent: monthlyNews.Content, + Question: poll.Question, + Link: poll.Link, + } + + return monthlyReport, nil +} diff --git a/internal/models/poll.go b/internal/models/poll.go new file mode 100644 index 0000000..76acbc8 --- /dev/null +++ b/internal/models/poll.go @@ -0,0 +1,152 @@ +package models + +import ( + "encoding/json" + "log" + "net/http" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" +) + +type Poll struct { + Year int `json:"year"` + Month int `json:"month"` + Question string `json:"question"` + Link string `json:"link"` +} + +// GetAllPolls godoc +// @Summary List all polls +// @Description Get details of all polls +// @Tags poll +// @Produce json +// @Success 200 {array} Poll +// @Router /api/admin/poll [get] +func (dh *DataHandler) GetAllPolls(w http.ResponseWriter, r *http.Request) { + var polls []Poll + err := dh.db.Find(&polls).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(polls) + log.Printf("| get all polls | %v", r.RemoteAddr) +} + +// GetSinglePoll godoc +// @Summary Get details of a specific poll +// @Description Get details of a specific poll +// @Tags poll +// @Produce json +// @Success 200 {object} Poll +// @Failure 404 {string} string "Not found" +// @Param year path int true "Year of the poll" +// @Param month path int true "Month of the poll" +// @Router /api/admin/poll/{year}/{month} [get] +func (dh *DataHandler) GetSinglePoll(w http.ResponseWriter, r *http.Request) { + year, month, err := common.YearMonthFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + + var poll Poll + err = dh.db.Where("year = ? AND month = ?", year, month).First(&poll).Error + if err != nil { + // If not found, answer "not found" + w.WriteHeader(http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(poll) +} + +// SavePoll godoc +// @Summary Update a specific poll' content +// @Description Update a specific poll' content +// @Tags poll +// @Accept json +// @Produce json +// @Success 200 {object} Poll "Updated successfully" +// @Success 201 {object} Poll "Created successfully" +// @Failure 400 {string} string "Bad Request" +// @Failure 500 {string} string "Internal server error" +// @Param poll body Poll true "Poll to update with new content" +// @Router /api/admin/poll [put] +func (dh *DataHandler) SavePoll(w http.ResponseWriter, r *http.Request) { + if r.Body == http.NoBody { + http.Error(w, "request body is empty", http.StatusBadRequest) + return + } + + decoder := json.NewDecoder(r.Body) + var poll Poll + err := decoder.Decode(&poll) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Check if this poll exists + err = dh.db.Where("year = ? AND month = ?", poll.Year, poll.Month).First(&Poll{}).Error + + if err != nil { + // Create a poll + err = dh.db.Create(&Poll{Year: poll.Year, Month: poll.Month, Question: poll.Question, Link: poll.Link}).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(poll) + log.Printf("| new poll | year : %d month : %d | %v", poll.Year, poll.Month, r.RemoteAddr) + return + + } else { + // Update question + err = dh.db.Model(&Poll{}).Where("year = ? AND month = ?", poll.Year, poll.Month).Update("question", poll.Question).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + // Update link + err = dh.db.Model(&Poll{}).Where("year = ? AND month = ?", poll.Year, poll.Month).Update("link", poll.Link).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(poll) + log.Printf("| updated poll year : %d month : %d | %v", poll.Year, poll.Month, r.RemoteAddr) + } +} + +// DeletePoll godoc +// @Summary Delete a specific poll +// @Description Delete a specific poll +// @Tags poll +// @Produce json +// @Success 200 {string} string "successful delete" +// @Failure 404 {string} string "Not found" +// @Param year path int true "Year of the poll" +// @Param month path int true "Month of the poll" +// @Router /api/admin/poll/{year}/{month} [delete] +func (dh *DataHandler) DeletePoll(w http.ResponseWriter, r *http.Request) { + year, month, err := common.YearMonthFromRequest(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = dh.db.Where("year = ? AND month = ?", year, month).Delete(&Poll{}).Error + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("successful delete")) +} diff --git a/internal/rootmux/rootmux.go b/internal/rootmux/rootmux.go new file mode 100644 index 0000000..8430785 --- /dev/null +++ b/internal/rootmux/rootmux.go @@ -0,0 +1,69 @@ +package rootmux + +import ( + "net/http" + + _ "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/docs" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/auth" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/file" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/models" + "github.com/gorilla/mux" + httpSwagger "github.com/swaggo/http-swagger" +) + +type RootMux struct { + Router *mux.Router + Manager *auth.Manager +} + +// @title Backoffice API +// @version 1.0 +// @description This is a sample service for managing newsletters for Ecolyo +// @termsOfService http://swagger.io/terms/ +// @contact.name API Support +// @contact.email rpailharey@grandlyon.com +// @host localhost:1443 +// @BasePath / + +func CreateRootMux() RootMux { + + r := mux.NewRouter() + m := auth.NewManager() + dh := models.NewDataHandler() + + r.HandleFunc("/OAuth2Login", m.HandleOAuth2Login) + r.Handle("/OAuth2Callback", m.HandleOAuth2Callback()) + r.HandleFunc("/Logout", m.HandleLogout) + r.Handle("/api/common/WhoAmI", auth.ValidateAuthMiddleware(auth.WhoAmI(), []string{"*"}, false)) + + r.HandleFunc("/api/common/monthlyReport", dh.GetCurrentMonthlyReport).Methods(http.MethodGet) + r.HandleFunc("/api/common/monthlyReport/{year}/{month}", dh.GetMonthlyReport).Methods(http.MethodGet) + + apiAdmin := r.PathPrefix("/api/admin").Subrouter() + apiAdmin.Use(auth.AdminAuthMiddleware) + + apiAdmin.HandleFunc("/monthlyNews", dh.GetAllMonthlyNews).Methods(http.MethodGet) + apiAdmin.HandleFunc("/monthlyNews/{year}/{month}", dh.GetSingleMonthlyNews).Methods(http.MethodGet) + apiAdmin.HandleFunc("/monthlyNews", dh.SaveMonthlyNews).Methods(http.MethodPut) + apiAdmin.HandleFunc("/monthlyNews/{year}/{month}", dh.DeleteMonthlyNews).Methods(http.MethodDelete) + + apiAdmin.HandleFunc("/monthlyInfo", dh.GetAllMonthlyInfo).Methods(http.MethodGet) + apiAdmin.HandleFunc("/monthlyInfo/{year}/{month}", dh.GetSingleMonthlyInfo).Methods(http.MethodGet) + apiAdmin.HandleFunc("/monthlyInfo", dh.SaveMonthlyInfo).Methods(http.MethodPut) + apiAdmin.HandleFunc("/monthlyInfo/{year}/{month}", dh.DeleteMonthlyInfo).Methods(http.MethodDelete) + + apiAdmin.HandleFunc("/poll", dh.GetAllPolls).Methods(http.MethodGet) + apiAdmin.HandleFunc("/poll/{year}/{month}", dh.GetSinglePoll).Methods(http.MethodGet) + apiAdmin.HandleFunc("/poll", dh.SavePoll).Methods(http.MethodPut) + apiAdmin.HandleFunc("/poll/{year}/{month}", dh.DeletePoll).Methods(http.MethodDelete) + + apiAdmin.HandleFunc("/imageNames", file.GetEcogestureImages).Methods(http.MethodGet) + + // Swagger + r.PathPrefix("/swagger").Handler(httpSwagger.WrapHandler) + + // Redirect route + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) + + return RootMux{r, &m} +} diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go new file mode 100644 index 0000000..9fa492d --- /dev/null +++ b/internal/rootmux/rootmux_test.go @@ -0,0 +1,196 @@ +package rootmux + +import ( + "encoding/json" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "os" + "testing" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/auth" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/mocks" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/models" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/tester" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/tokens" +) + +var ( + oAuth2Server *httptest.Server + monthlyInfo = models.MonthlyInfo{Year: 2021, Month: 0, Info: "Informations du mois", Image: "imagebase64"} + monthlyInfoStr string + monthlyNews = models.MonthlyNews{Year: 2021, Month: 0, Title: "", Content: "Nouvelles fonctionnalités"} + monthlyNewsStr string + newPoll = models.Poll{Year: 2021, Month: 0, Question: "pollQuestion", Link: "pollLink"} + newPollStr string + noH map[string]string +) + +func TestMain(m *testing.M) { + + // Create the OAuth2 mock server + oAuth2Server = httptest.NewServer(mocks.CreateMockOAuth2()) + defer oAuth2Server.Close() + + // Set the constants with environment variables + os.Setenv("HOSTNAME", "localhost") + os.Setenv("ADMIN_ROLE", "ADMINS") + os.Setenv("CLIENT_ID", "foo") + os.Setenv("CLIENT_SECRET", "bar") + os.Setenv("TOKEN_URL", oAuth2Server.URL+"/token") + os.Setenv("USERINFO_URL", oAuth2Server.URL+"/userinfo") + os.Setenv("LOGOUT_URL", oAuth2Server.URL+"/logout") + + // Setup the token manager to use debug mode + os.Setenv("DEBUG_MODE", "true") + tokens.Init("../configs/tokenskey.json", true) + + // Convert example objects to string + monthlyNewsBytes, _ := json.Marshal(monthlyNews) + monthlyNewsStr = string(monthlyNewsBytes) + monthlyInfoBytes, _ := json.Marshal(monthlyInfo) + monthlyInfoStr = string(monthlyInfoBytes) + newPollBytes, _ := json.Marshal(newPoll) + newPollStr = string(newPollBytes) + + code := m.Run() + // Remove the database + os.Remove("backoffice.db") + os.Exit(code) +} + +func TestAll(t *testing.T) { + + // Set up testers + os.Setenv("AUTH_URL", oAuth2Server.URL+"/auth-wrong-state") // Set the server to access failing OAuth2 endpoints + oauth2Tests(t) + os.Setenv("AUTH_URL", oAuth2Server.URL+"/auth") // Set the server to access the correct OAuth2Endpoint + unloggedTests(t) + + os.Setenv("USERINFO_URL", oAuth2Server.URL+"/admininfo") + adminTests(t) + +} + +/** +SECURITY TESTS (this tests are to check that the security protections works) +**/ +func oauth2Tests(t *testing.T) { + // Create the tester + ts, do, _ := createTester(t) + defer ts.Close() // Close the tester + // Try to login (must fail) + do("GET", "/OAuth2Login", noH, "", http.StatusInternalServerError, "invalid oauth state") +} + +/** +UNLOGGED USER TESTS (this tests are to check that the security protections works) +**/ +func unloggedTests(t *testing.T) { + // Create the tester + ts, do, _ := createTester(t) + defer ts.Close() // Close the tester + + // Try to create a monthlyNews (must fail) + do("PUT", "/api/admin/monthlyNews", noH, monthlyNewsStr, http.StatusUnauthorized, "error extracting token") + // Try to get the most recent monthlyReport (must fail because not found) + do("GET", "/api/common/monthlyReport", noH, "", http.StatusNotFound, "") +} + +/** +ADMIN TESTS (this tests are to check that an administrator can edit a newsletter's content) +**/ +func adminTests(t *testing.T) { + // Create the tester + ts, do, _ := createTester(t) + defer ts.Close() // Close the tester + tests := func() { + // Get the XSRF Token + response := do("GET", "/api/common/WhoAmI", noH, "", http.StatusOK, "") + token := auth.TokenData{} + json.Unmarshal([]byte(response), &token) + xsrfHeader := map[string]string{"XSRF-TOKEN": token.XSRFToken} + // Try to create a monthlyNews without the XSRF-TOKEN (must fail) + do("PUT", "/api/admin/monthlyNews", noH, monthlyNewsStr, http.StatusUnauthorized, "XSRF protection triggered") + // Try to create a monthlyNews without body (must fail) + do("PUT", "/api/admin/monthlyNews", xsrfHeader, "", http.StatusBadRequest, "request body is empty") + // Try to get a monthlyNews before it is created (must fail because not found) + do("GET", "/api/admin/monthlyNews/2021/0", xsrfHeader, "", http.StatusNotFound, "") + // Try to create a monthlyNews (must pass) + do("PUT", "/api/admin/monthlyNews", xsrfHeader, monthlyNewsStr, http.StatusCreated, `{"year":2021,"month":0,"title":"Les nouveautés du service","content":"Nouvelles fonctionnalités"`) + // Try to update a monthlyNews (must pass) + do("PUT", "/api/admin/monthlyNews", xsrfHeader, monthlyNewsStr, http.StatusOK, `{"year":2021,"month":0,"title":"Les nouveautés du service","content":"Nouvelles fonctionnalités"`) + // Try to get the monthlyNews created (must pass) + do("GET", "/api/admin/monthlyNews/2021/0", xsrfHeader, "", http.StatusOK, `{"year":2021,"month":0,"title":"Les nouveautés du service","content":"Nouvelles fonctionnalités"`) + // Try to get the monthlyReport (must fail because monthlyInfo not found) + do("GET", "/api/common/monthlyReport/2021/0", noH, "", http.StatusNotFound, "") + + // Try to create a poll without the XSRF-TOKEN (must fail) + do("PUT", "/api/admin/poll", noH, newPollStr, http.StatusUnauthorized, "XSRF protection triggered") + // Try to create a poll without body (must fail) + do("PUT", "/api/admin/poll", xsrfHeader, "", http.StatusBadRequest, "request body is empty") + // Try to get a poll before it is created (must fail because not found') + do("GET", "/api/admin/poll/2021/0", xsrfHeader, "", http.StatusNotFound, "") + // Try to create a poll (must pass) + do("PUT", "/api/admin/poll", xsrfHeader, newPollStr, http.StatusCreated, newPollStr) + // Try to update a poll (must pass) + do("PUT", "/api/admin/poll", xsrfHeader, newPollStr, http.StatusOK, newPollStr) + // Try to get the poll created (must pass) + do("GET", "/api/admin/poll/2021/0", xsrfHeader, "", http.StatusOK, newPollStr) + // Try to get the monthlyReport (must fail because monthlyInfo not found) + do("GET", "/api/common/monthlyReport/2021/0", noH, "", http.StatusNotFound, "") + + // Try to create a monthlyInfo without the XSRF-TOKEN (must fail) + do("PUT", "/api/admin/monthlyInfo", noH, monthlyInfoStr, http.StatusUnauthorized, "XSRF protection triggered") + // Try to create a monthlyInfo without body (must fail) + do("PUT", "/api/admin/monthlyInfo", xsrfHeader, "", http.StatusBadRequest, "request body is empty") + // Try to get a monthlyInfo before it is created (must fail because not found) + do("GET", "/api/admin/monthlyInfo/2021/0", xsrfHeader, "", http.StatusNotFound, "") + // Try to create a monthlyInfo (must pass) + do("PUT", "/api/admin/monthlyInfo", xsrfHeader, monthlyInfoStr, http.StatusCreated, monthlyInfoStr) + // Try to update a monthlyInfo (must pass) + do("PUT", "/api/admin/monthlyInfo", xsrfHeader, monthlyInfoStr, http.StatusOK, monthlyInfoStr) + // Try to get the monthlyInfo created (must pass) + do("GET", "/api/admin/monthlyInfo/2021/0", xsrfHeader, "", http.StatusOK, monthlyInfoStr) + // Try to get the monthlyReport (must pass) + do("GET", "/api/common/monthlyReport/2021/0", noH, "", http.StatusOK, `{"year":2021,"month":0,"info":"Informations du mois","image":"imagebase64","newsTitle":"Les nouveautés du service","newsContent":"Nouvelles fonctionnalités","question":"pollQuestion","link":"pollLink"`) + + // Try to delete the monthlyNews created (must pass) + do("DELETE", "/api/admin/monthlyNews/2021/0", xsrfHeader, "", http.StatusOK, "successful delete") + // Try to get a monthlyNews after it is deleted (must fail because not found) + do("GET", "/api/admin/monthlyNews/2021/0", xsrfHeader, "", http.StatusNotFound, "") + // Try to get the monthlyReport (must pass) + do("GET", "/api/common/monthlyReport/2021/0", noH, "", http.StatusOK, `{"year":2021,"month":0,"info":"Informations du mois","image":"imagebase64","newsTitle":"","newsContent":"","question":"pollQuestion","link":"pollLink"`) + // Try to delete the poll created (must pass) + do("DELETE", "/api/admin/poll/2021/0", xsrfHeader, "", http.StatusOK, "successful delete") + // Try to get a poll after it is deleted (must fail because not found) + do("GET", "/api/admin/poll/2021/0", xsrfHeader, "", http.StatusNotFound, "") + // Try to get the monthlyReport (must pass) + do("GET", "/api/common/monthlyReport/2021/0", noH, "", http.StatusOK, `{"year":2021,"month":0,"info":"Informations du mois","image":"imagebase64","newsTitle":"","newsContent":"","question":"","link":""`) + } + // Try to login (must pass) + do("GET", "/OAuth2Login", noH, "", http.StatusOK, "") + // Run the tests + tests() + // Try to logout (must pass) + do("GET", "/Logout", noH, "", http.StatusOK, "") + // Try to get a monthlyNews again (must fail) + do("GET", "/api/admin/monthlyNews", noH, "", http.StatusUnauthorized, "error extracting token") + // Try to get a poll again (must fail) + do("GET", "/api/admin/poll", noH, "", http.StatusUnauthorized, "error extracting token") +} + +func createTester(t *testing.T) (*httptest.Server, tester.DoFn, tester.DoFn) { + // Create the server + mux := CreateRootMux() + ts := httptest.NewServer(mux.Router) + url, _ := url.Parse(ts.URL) + port := url.Port() + mux.Manager.Config.RedirectURL = "http://" + os.Getenv("HOSTNAME") + ":" + port + "/OAuth2Callback" + mux.Manager.Hostname = "http://" + os.Getenv("HOSTNAME") + ":" + port + // Create the cookie jar + jar, _ := cookiejar.New(nil) + // wrap the testing function + return ts, tester.CreateServerTester(t, port, os.Getenv("HOSTNAME"), jar), tester.CreateServerTester(t, port, os.Getenv("HOSTNAME"), nil) +} diff --git a/internal/tester/tester.go b/internal/tester/tester.go new file mode 100644 index 0000000..d7c6a67 --- /dev/null +++ b/internal/tester/tester.go @@ -0,0 +1,92 @@ +package tester + +import ( + "context" + "io/ioutil" + "net" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" +) + +type DoFn func(method string, url string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string + +// DoRequestOnHandler does a request on a router (or handler) and check the response +func DoRequestOnHandler(t *testing.T, router http.Handler, method string, route string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string { + req, err := http.NewRequest(method, route, strings.NewReader(payload)) + if err != nil { + t.Fatal(err) + } + for i, v := range headers { + req.Header.Set(i, v) + } + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + if status := rr.Code; status != expectedStatus { + t.Errorf("Tested %v %v %v ; handler returned wrong status code: got %v want %v", method, route, payload, status, expectedStatus) + } + if !strings.HasPrefix(rr.Body.String(), expectedBody) { + t.Errorf("Tested %v %v %v ; handler returned unexpected body: got %v want %v", method, route, payload, rr.Body.String(), expectedBody) + } + return string(rr.Body.String()) +} + +// DoRequestOnServer does a request on listening server +func DoRequestOnServer(t *testing.T, hostname string, port string, jar *cookiejar.Jar, method string, testURL string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string { + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + } + // or create your own transport, there's an example on godoc. + http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + addrAndPort := strings.Split(addr, ":") + if strings.HasSuffix(addrAndPort[0], "vestibule.io") { + addr = "127.0.0.1:" + addrAndPort[1] + } + return dialer.DialContext(ctx, network, addr) + } + if strings.HasPrefix(testURL, "/") { + testURL = "http://" + hostname + ":" + port + testURL + } else { + u, _ := url.Parse("http://" + testURL) + testURL = "http://" + u.Host + ":" + port + u.Path + "?" + u.RawQuery + } + req, err := http.NewRequest(method, testURL, strings.NewReader(payload)) + if err != nil { + t.Fatal(err) + } + for i, v := range headers { + req.Header.Set(i, v) + } + var client *http.Client + if jar != nil { + client = &http.Client{Jar: jar} + } else { + client = &http.Client{} + } + res, err := client.Do(req) + if err != nil { + t.Fatal(err) + } + body, _ := ioutil.ReadAll(res.Body) + bodyString := string(body) + if status := res.StatusCode; status != expectedStatus { + t.Errorf("Tested %v %v %v ; handler returned wrong status code: got %v want %v", method, testURL, payload, status, expectedStatus) + } + if !strings.HasPrefix(bodyString, expectedBody) { + t.Errorf("Tested %v %v %v ; handler returned unexpected body: got %v want %v", method, testURL, payload, bodyString, expectedBody) + } + return bodyString +} + +// CreateServerTester wraps DoRequestOnServer to factorize t, port and jar +func CreateServerTester(t *testing.T, hostname string, port string, jar *cookiejar.Jar) DoFn { + return func(method string, url string, headers map[string]string, payload string, expectedStatus int, expectedBody string) string { + return DoRequestOnServer(t, port, hostname, jar, method, url, headers, payload, expectedStatus, expectedBody) + } +} diff --git a/internal/tokens/tokens.go b/internal/tokens/tokens.go new file mode 100644 index 0000000..c22225f --- /dev/null +++ b/internal/tokens/tokens.go @@ -0,0 +1,232 @@ +package tokens + +import ( + "bytes" + "compress/flate" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "strings" + "time" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" +) + +var ( + now = time.Now + // m is the current token manager + m manager +) + +// manager manages tokens +type manager struct { + key []byte + debugMode bool +} + +// Init inits the main token manager +func Init(keyfile string, debug bool) { + m = newManager(keyfile, debug) +} + +// newManager creates a manager +func newManager(keyfile string, debug bool) manager { + var keyConfig struct { + Key []byte + } + err := common.Load(keyfile, &keyConfig) + if err != nil { + keyConfig.Key, err = common.GenerateRandomBytes(32) + if err != nil { + log.Fatal(err) + } + err := common.Save(keyfile, keyConfig) + if err != nil { + log.Println("Token signing key could not be saved") + } + } + log.Println("Token signing key set") + return manager{ + debugMode: debug, + key: keyConfig.Key, + } +} + +// Token represents a token containting data +type Token struct { + ExpiresAt int64 + IssuedAt int64 `json:"iat,omitempty"` + Data []byte +} + +// CreateCookie creates a token with the given data and returns it in a cookie +func CreateCookie(data interface{}, hostName string, cookieName string, duration time.Duration, w http.ResponseWriter) { + expiration := now().Add(duration) + value, err := CreateToken(data, expiration) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + cookie := http.Cookie{Name: cookieName, Domain: hostName, Value: value, Expires: expiration, Secure: !m.debugMode, HttpOnly: true, SameSite: http.SameSiteLaxMode} + http.SetCookie(w, &cookie) +} + +// CreateToken creates a token with the given data +func CreateToken(data interface{}, expiration time.Time) (string, error) { + // Marshall the data + d, err := json.Marshal(data) + if err != nil { + return "", err + } + // Create the payload + token := Token{ + ExpiresAt: expiration.Unix(), + Data: d, + } + // Serialize the payload + sToken, err := json.Marshal(token) + if err != nil { + return "", err + } + // Compress with deflate + var csToken bytes.Buffer + var c *flate.Writer + if c, err = flate.NewWriter(&csToken, flate.BestCompression); err != nil { + return "", err + } + if _, err := c.Write(sToken); err != nil { + return "", err + } + if err := c.Close(); err != nil { + return "", err + } + ecsToken, err := Encrypt(csToken.Bytes(), m.key) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(ecsToken), nil +} + +// ExtractAndValidateToken extracts the token from the request, validates it, and return the data n the value pointed to by v +func ExtractAndValidateToken(r *http.Request, cookieName string, v interface{}, checkXSRF bool) (bool, error) { + becsToken, checkXSRF, err := func(r *http.Request, checkXSRF bool) (string, bool, error) { + // Try to extract from the query + query := r.URL.Query().Get("token") + if query != "" { + return query, false, nil + } + // Try to extract from the cookie + cookie, err := r.Cookie(cookieName) + if err == nil { + return cookie.Value, checkXSRF, err + } + // Try to get an auth token from the header + authHeader := strings.Split(r.Header.Get("Authorization"), " ") + if authHeader[0] == "Bearer" && len(authHeader) == 2 { + return authHeader[1], false, nil + } + // Try to use the basic auth header instead + if authHeader[0] == "Basic" && len(authHeader) == 2 { + decoded, err := base64.StdEncoding.DecodeString(authHeader[1]) + if err == nil { + auth := strings.Split(string(decoded), ":") + return auth[1], false, nil + } + } + return "", false, errors.New("could not extract token") + }(r, checkXSRF) + + if err == nil { + return checkXSRF, unstoreData(becsToken, v) + } + return false, err +} + +// unstoreData decrypt, uncompress, unserialize the token, and returns the data n the value pointed to by v +func unstoreData(becsToken string, v interface{}) error { + // Decrypt the token + ecsToken, err := base64.StdEncoding.DecodeString(becsToken) + if err != nil { + return fmt.Errorf("failed to unbase64 token") + + } + csToken, err := Decrypt(ecsToken, m.key) + if err != nil { + return fmt.Errorf("failed to decrypt token") + + } + // Uncompress the token + rdata := bytes.NewReader(csToken) + r := flate.NewReader(rdata) + sToken, err := ioutil.ReadAll(r) + if err != nil { + return fmt.Errorf("failed to uncompress token") + } + // Unserialize the token + token := Token{} + err = json.Unmarshal(sToken, &token) + if err != nil { + return fmt.Errorf("failed to unmarshall token") + + } + // Validate the token + if token.ExpiresAt < now().Unix() { + return fmt.Errorf("token expired") + } + // Update the data + err = json.Unmarshal(token.Data, v) + if err != nil { + return fmt.Errorf("failed to unmarshall data") + + } + // Return no error if everything is fine + return nil +} + +// Encrypt a byte array with AES +func Encrypt(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return []byte{}, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return []byte{}, err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return []byte{}, err + } + cipherData := gcm.Seal(nonce, nonce, data, nil) + return cipherData, nil +} + +// Decrypt a byte array with AES +func Decrypt(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return []byte{}, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return []byte{}, err + } + nonceSize := gcm.NonceSize() + if len(data) <= nonceSize { + return []byte{}, err + } + nonce, cipherData := data[:nonceSize], data[nonceSize:] + plainData, err := gcm.Open(nil, nonce, cipherData, nil) + if err != nil { + return []byte{}, err + } + return plainData, nil +} diff --git a/internal/tokens/tokens_test.go b/internal/tokens/tokens_test.go new file mode 100644 index 0000000..841819a --- /dev/null +++ b/internal/tokens/tokens_test.go @@ -0,0 +1,64 @@ +package tokens + +import ( + "fmt" + "testing" + "time" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" +) + +type user struct { + Login string + Password string +} + +func (u user) String() string { + return fmt.Sprintf("Login: %v, Password: %v", u.Login, u.Password) +} + +func TestManagerCreateTokenUnStoreData(t *testing.T) { + key, _ := common.GenerateRandomBytes(32) + key2, _ := common.GenerateRandomBytes(32) + type fields struct { + encryptKey []byte + decryptKey []byte + debugMode bool + } + type args struct { + data interface{} + expiration time.Time + } + tests := []struct { + name string + fields fields + args args + want bool + wantErr bool + }{ + {"future_expiration", fields{key, key, false}, args{user{"admin", "password"}, time.Now().Add(24 * time.Hour)}, true, false}, + {"past_expiration", fields{key, key, false}, args{user{"admin", "password"}, time.Now().Add(-24 * time.Hour)}, false, true}, + {"incorrect_aes_key", fields{[]byte("wrong_key_size"), []byte("wrong_key_size"), false}, args{user{"admin", "password"}, time.Now().Add(+24 * time.Hour)}, false, true}, + {"wrong_decrypt_key", fields{key, key2, false}, args{user{"admin", "password"}, time.Now().Add(+24 * time.Hour)}, false, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m = manager{ + key: tt.fields.encryptKey, + debugMode: tt.fields.debugMode, + } + token, _ := CreateToken(tt.args.data, tt.args.expiration) + m.key = tt.fields.decryptKey + v := user{} + err := unstoreData(token, &v) + got := tt.args.data == v + if (err != nil) != tt.wantErr { + t.Errorf("manager.(un)storeData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("manager.(un)storeData() inData:%v, outData:%v => equality: %v, want %v", tt.args.data, v, got, tt.want) + } + }) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2876f92 --- /dev/null +++ b/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + "log" + + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/common" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/mocks" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/rootmux" + "forge.grandlyon.com/web-et-numerique/llle_project/backoffice-server/internal/tokens" +) + +var ( + httpsPort = common.IntValueFromEnv("HTTPS_PORT", 443) // HTTPS port to serve on + debugMode = common.BoolValueFromEnv("DEBUG_MODE", false) // Debug mode, disable Secure attribute for cookies + mockOAuth2 = common.BoolValueFromEnv("MOCK_OAUTH2", false) // Enable mock OAuth2 login +) + +func main() { + + log.Println("--- Server is starting ---") + + // Initializations + tokens.Init("./configs/tokenskey.json", debugMode) + + // Create the server + rootMux := rootmux.CreateRootMux() + + // Init the hostname + mocks.Init(httpsPort) + + // Start a mock oauth2 server if debug mode is on + if mockOAuth2 { + mockOAuth2Port := ":8090" + go http.ListenAndServe(mockOAuth2Port, mocks.CreateMockOAuth2()) + fmt.Println("Mock OAuth2 server Listening on: http://localhost" + mockOAuth2Port) + } + + // Serve locally with https + log.Fatal(http.ListenAndServeTLS(":"+strconv.Itoa(httpsPort), "./dev_certificates/localhost.crt", "./dev_certificates/localhost.key", rootMux.Router)) +} diff --git a/template.env b/template.env new file mode 100644 index 0000000..46c5957 --- /dev/null +++ b/template.env @@ -0,0 +1,20 @@ +# Common settings +HOSTNAME= +ADMIN_ROLE= +DEBUG_MODE= +MOCK_OAUTH2= +HTTPS_PORT= +IMAGE_FOLDER= + +# Needed to user OAuth2 authentication : +REDIRECT_URL= +CLIENT_ID= +CLIENT_SECRET= +AUTH_URL= +TOKEN_URL= +USERINFO_URL= + +# Access to the database +DATABASE_USER= +DATABASE_PASSWORD= +DATABASE_NAME= \ No newline at end of file -- GitLab