This commit is contained in:
Florian Hoss 2023-07-04 11:51:13 +02:00
commit f90fdc0598
99 changed files with 15260 additions and 0 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

576
.gitignore vendored Normal file
View file

@ -0,0 +1,576 @@
### JetBrains template
.idea/
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Go template
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
### VisualStudio template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
storage/
docs/
dist/
tmp/
docker/**/*.sqlite3
docker/**/*.txt

9
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,9 @@
stages:
- build
- deploy
include:
- local: .gitlab/_common.gitlab-ci.yml
- local: .gitlab/_rules.gitlab-ci.yml
- local: /.gitlab/build.gitlab-ci.yml
- local: /.gitlab/deploy.gitlab-ci.yml

View file

@ -0,0 +1,14 @@
variables:
LATEST_IMAGE: '$CI_REGISTRY_IMAGE:latest'
DOCKER_VERSION: '24.0.2'
GOLANG_VERSION: '1.20'
NODE_VERSION: '18'
ALPINE_VERSION: '3'
DEBIAN_VERSION: '12'
TELEPORT_VERSION: '13.1.1'
image: docker:$DOCKER_VERSION-git
.login_registry:
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY

View file

@ -0,0 +1,17 @@
.if-release-candidate-tag: &if-release-candidate-tag
if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+/'
.if-stable-release-tag: &if-stable-release-tag
if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/'
.if-default-branch: &if-default-branch
if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
.rules:release:
rules:
- <<: *if-release-candidate-tag
- <<: *if-stable-release-tag
.rules:default:
rules:
- <<: *if-default-branch

View file

@ -0,0 +1,24 @@
build_release:
rules: !reference [.rules:release, rules]
stage: build
extends: .login_registry
services:
- name: docker:$DOCKER_VERSION-dind
alias: docker
variables:
DOCKER_TLS_CERTDIR: '/certs'
CURRENT_IMAGE: '$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG'
script:
- >
docker build .
--file docker/Dockerfile
--build-arg GOLANG_VERSION=$GOLANG_VERSION
--build-arg NODE_VERSION=$NODE_VERSION
--build-arg ALPINE_VERSION=$ALPINE_VERSION
--build-arg APP_VERSION=$CI_COMMIT_TAG
--build-arg BUILD_TIME=$CI_JOB_STARTED_AT
--tag $CURRENT_IMAGE
--tag $LATEST_IMAGE
- docker inspect $CURRENT_IMAGE
- docker push $CURRENT_IMAGE
- docker push $LATEST_IMAGE

View file

@ -0,0 +1,18 @@
deploy_release:
rules: !reference [.rules:release, rules]
stage: deploy
image: debian:${DEBIAN_VERSION}-slim
id_tokens:
TBOT_GITLAB_JWT:
aud: tp.fhoss.de
script:
- apt-get update && apt-get install curl -y
- cd /tmp
- 'curl -O https://cdn.teleport.dev/teleport-v${TELEPORT_VERSION}-linux-amd64-bin.tar.gz'
- tar -xvf teleport-v${TELEPORT_VERSION}-linux-amd64-bin.tar.gz
- ./teleport/install
- 'tbot start --token=gitlab --destination-dir=/tmp/tbot-user --data-dir=/tmp/tbot-data --auth-server=tp.fhoss.de:443 --join-method=gitlab --oneshot'
- 'tsh -i /tmp/tbot-user/identity --proxy tp.fhoss.de:443 ssh bot@cafe "docker compose -f /opt/docker/cafe/docker-compose.yml up -d --pull always"'
- 'tsh -i /tmp/tbot-user/identity --proxy tp.fhoss.de:443 ssh bot@cafe "docker system prune --force"'
environment:
name: production

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Florian Hoss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

76
README.md Normal file
View file

@ -0,0 +1,76 @@
# Café Plätschwiesle
![Alt vue](https://img.shields.io/badge/Framework-Vue3-informational?style=for-the-badge&logo=vuedotjs&color=4FC08D)
![Alt typescript](https://img.shields.io/badge/Language-Typescript-informational?style=for-the-badge&logo=typescript&color=3178C6)
![Alt go](https://img.shields.io/badge/Language-Go-informational?style=for-the-badge&logo=go&color=00ADD8)
## docker-compose example
```yaml
services:
cafe:
image: ghcr.io/flohoss/cafe-plaetschwiesle:latest
container_name: cafe
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- ALLOWED_HOSTS=http://localhost:5000,https://home.example.com
- SWAGGER=true
- LOG_LEVEL=info # trace,debug,info,warn,error,fatal,panic
volumes:
- ./storage:/app/storage
ports:
- '127.0.0.1:5000:5000'
```
## docker-compose example with MariaDB as database
```yaml
networks:
net:
external: false
services:
cafe-db:
image: lscr.io/linuxserver/mariadb:latest
container_name: cafe-db
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- MYSQL_ROOT_PASSWORD=root
- TZ=Europe/Berlin
- MYSQL_DATABASE=db
- MYSQL_USER=user
- MYSQL_PASSWORD=password
volumes:
- ./db:/config
expose:
- 3306
networks:
- net
cafe:
image: ghcr.io/flohoss/cafe-plaetschwiesle:latest
container_name: cafe
restart: unless-stopped
depends_on:
- cafe-db
environment:
- PUID=1000
- PGID=1000
- ALLOWED_HOSTS=http://localhost:5000,https://home.example.com
- SWAGGER=true
- LOG_LEVEL=info # trace,debug,info,warn,error,fatal,panic
- MYSQL_URL=cafe-db:3306
- MYSQL_USER=user
- MYSQL_PASSWORD=password
- MYSQL_DATABASE=db
volumes:
- ./storage:/app/storage
ports:
- '127.0.0.1:5000:5000'
networks:
- net
```

52
api/middlwares.go Normal file
View file

@ -0,0 +1,52 @@
package api
import (
"cafe/config"
"net/http"
"strings"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func myLogger() gin.HandlerFunc {
return func(c *gin.Context) {
if logrus.GetLevel() != logrus.TraceLevel {
return
}
reqUri := c.Request.RequestURI
if strings.Contains(reqUri, "/storage") {
return
}
startTime := time.Now()
c.Next()
endTime := time.Now()
latencyTime := endTime.Sub(startTime)
logrus.WithFields(logrus.Fields{
"status": http.StatusText(c.Writer.Status()),
"latency": latencyTime,
"client": c.ClientIP(),
"method": c.Request.Method,
}).Trace(reqUri)
}
}
func authHeader() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Remote-Groups", c.Request.Header.Get("Remote-Groups"))
c.Writer.Header().Set("Remote-Name", c.Request.Header.Get("Remote-Name"))
}
}
func (a *Api) SetMiddlewares() {
a.Router.Use(myLogger())
a.Router.Use(gin.Recovery())
a.Router.Use(cors.Default())
_ = a.Router.SetTrustedProxies(nil)
a.Router.MaxMultipartMemory = 8 << 20 // 8 MiB
logrus.WithFields(logrus.Fields{
"allowedOrigins": config.Cafe.AllowedHosts,
}).Debug("Middlewares set")
}

62
api/router.go Normal file
View file

@ -0,0 +1,62 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func (a *Api) SetupRouter() {
api := a.Router.Group("/api")
{
tableGroup := api.Group("/tables")
{
tableGroup.GET("", a.getTables)
tableGroup.POST("", a.createTable)
tableGroup.DELETE("", a.deleteTable)
}
orderGroup := api.Group("/orders")
{
orderGroup.GET("", a.getOrders)
orderGroup.POST("", a.createOrder)
orderGroup.DELETE("", a.deleteOrder)
orderGroup.PUT("", a.updateOrder)
orderGroup.GET("/ws", a.serveWs)
orderItemGroup := orderGroup.Group("/items")
{
orderItemGroup.GET("", a.getOrderItems)
orderItemGroup.POST("", a.createOrderItem)
orderItemGroup.PUT("", a.updateOrderItem)
orderItemGroup.DELETE("/:id", a.deleteOrderItem)
}
}
billGroup := api.Group("/bills")
{
billGroup.GET("", a.getBills)
billGroup.POST("", a.createBill)
billGroup.DELETE("/:id", a.deleteBill)
billItemGroup := billGroup.Group("/items")
{
billItemGroup.GET("", a.getBillItems)
}
}
userGroup := api.Group("/users")
{
userGroup.GET("/:username", a.getUser)
userGroup.PUT("", a.updateUser)
}
health := api.Group("/health")
{
health.Use(authHeader())
health.GET("", func(c *gin.Context) {
c.Status(http.StatusOK)
})
}
}
a.Router.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
logrus.WithField("amount", len(a.Router.Routes())).Debug("Routes initialized")
}

112
api/routesBill.go Normal file
View file

@ -0,0 +1,112 @@
package api
import (
"cafe/service"
"cafe/types"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
// @Schemes
// @Summary get all bills
// @Description gets all bills as array
// @Tags bills
// @Produce json
// @Param year query int true "year"
// @Param month query int true "month (1-12)"
// @Param day query int true "day (1-31)"
// @Success 200 {array} service.Bill
// @Router /bills [get]
func (a *Api) getBills(c *gin.Context) {
year, presentYear := c.GetQuery("year")
month, presentMonth := c.GetQuery("month")
day, presentDay := c.GetQuery("day")
if !presentYear || !presentMonth || !presentDay {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
bills, err := service.GetAllBills(year, month, day)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, bills)
}
}
// @Schemes
// @Summary get all billItems
// @Description gets all billItems for bill
// @Tags bills
// @Produce json
// @Param bill query int true "Bill ID"
// @Success 200 {array} service.BillItem
// @Router /bills/items [get]
func (a *Api) getBillItems(c *gin.Context) {
bill, err := service.DoesBillExist(c.Query("bill"))
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
billItems, err := service.GetAllBillItems(bill.ID)
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, billItems)
}
}
// @Schemes
// @Summary create new bill
// @Description creates a new bill and returns it
// @Tags bills
// @Produce json
// @Param table query int true "Table ID"
// @Param filter query string false "filter"
// @Success 201 {object} service.Bill
// @Failure 404 "Not Found"
// @Failure 500 {object} errorResponse
// @Router /bills [post]
func (a *Api) createBill(c *gin.Context) {
table, tableErr := strconv.ParseUint(c.Query("table"), 10, 64)
if tableErr != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
stringFiler, filterPresent := c.GetQuery("filter")
var filter []string
if filterPresent {
filter = strings.Split(stringFiler, ",")
}
bill, err := service.CreateBill(service.GetOrderOptions{TableId: table, Grouped: true, Filter: filter})
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
}
c.JSON(http.StatusCreated, bill)
}
// @Schemes
// @Summary delete a bill
// @Description deletes a bill
// @Tags bills
// @Produce json
// @Param id path int true "Bill ID"
// @Success 200 "OK"
// @Failure 404 "Not Found"
// @Failure 500 {object} errorResponse
// @Router /bills/{id} [delete]
func (a *Api) deleteBill(c *gin.Context) {
id := c.Param("id")
bill, err := service.DoesBillExist(id)
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = service.DeleteBill(&bill)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
}
c.Status(http.StatusOK)
}

258
api/routesOrder.go Normal file
View file

@ -0,0 +1,258 @@
package api
import (
"cafe/hub"
"cafe/service"
"cafe/types"
ws "cafe/websocket"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
)
// @Schemes
// @Summary get all orders
// @Description gets all orders as array
// @Tags orders
// @Produce json
// @Param table query int false "Table ID"
// @Param grouping query bool false "grouping"
// @Param filter query string false "filter"
// @Success 200 {array} service.Order
// @Router /orders [get]
func (a *Api) getOrders(c *gin.Context) {
table, _ := strconv.ParseUint(c.Query("table"), 10, 64)
grouping, _ := strconv.ParseBool(c.Query("grouping"))
stringFiler, filterPresent := c.GetQuery("filter")
var filter []string
if filterPresent {
filter = strings.Split(stringFiler, ",")
}
options := service.GetOrderOptions{TableId: table, Grouped: grouping, Filter: filter}
var orders []service.Order
if options.TableId == 0 {
orders = service.GetAllActiveOrders()
} else {
orders = service.GetAllOrdersForTable(options)
}
c.JSON(http.StatusOK, orders)
}
// @Schemes
// @Summary create new order
// @Description creates a new order and returns it
// @Tags orders
// @Accept json
// @Produce json
// @Param item query int true "OrderItem ID"
// @Param table query int true "Table ID"
// @Success 201 {object} service.Order
// @Failure 400 {object} errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders [post]
func (a *Api) createOrder(c *gin.Context) {
table, err1 := strconv.ParseUint(c.Query("table"), 10, 64)
item, err2 := strconv.ParseUint(c.Query("item"), 10, 64)
if err1 != nil || err2 != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
order := service.Order{TableID: table, OrderItemID: item, IsServed: false}
err := service.CreateOrder(&order)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusCreated, order)
}
}
// @Schemes
// @Summary delete an order
// @Description deletes an order from the database
// @Tags orders
// @Produce json
// @Param item query int true "OrderItem ID"
// @Param table query int true "Table ID"
// @Success 200 "OK"
// @Failure 400 {object} errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders [delete]
func (a *Api) deleteOrder(c *gin.Context) {
item := c.Query("item")
table := c.Query("table")
if table == "" || item == "" {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
err := service.DeleteOrder(table, item)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.Status(http.StatusOK)
}
}
// @Schemes
// @Summary update an order
// @Description updates an order with provided information
// @Tags orders
// @Accept json
// @Produce json
// @Param order body service.Order true "updated Order"
// @Success 200 {object} service.Order
// @Failure 400 {object} errorResponse
// @Failure 404 "Not Found" errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders [put]
func (a *Api) updateOrder(c *gin.Context) {
var newOrder service.Order
err := c.ShouldBindJSON(&newOrder)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
oldOrder, err := service.DoesOrderExist(strconv.Itoa(int(newOrder.ID)))
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = service.UpdateOrder(&oldOrder, &newOrder)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, newOrder)
}
}
func (a *Api) serveWs(c *gin.Context) {
conn, err := ws.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logrus.WithField("error", err).Warning("Cannot upgrade websocket")
return
}
messageChan := make(hub.NotifierChan)
a.Hub.NewClients <- messageChan
defer func() {
a.Hub.ClosingClients <- messageChan
conn.Close()
}()
go ws.ReadPump(conn)
for {
msg, ok := <-messageChan
if !ok {
err := conn.WriteMessage(websocket.CloseMessage, []byte{})
if err != nil {
return
}
return
}
err := conn.WriteJSON(msg)
if err != nil {
return
}
}
}
// @Schemes
// @Summary get all orderItems
// @Description gets all orderItems as array
// @Tags orderItems
// @Produce json
// @Param type query int true "ItemType"
// @Success 200 {array} service.OrderItem
// @Router /orders/items [get]
func (a *Api) getOrderItems(c *gin.Context) {
orderType := c.Query("type")
if orderType == "" {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
} else {
c.JSON(http.StatusOK, service.GetOrderItemsForType(orderType))
}
}
// @Schemes
// @Summary create new orderItem
// @Description creates a new orderItem and returns it
// @Tags orderItems
// @Accept json
// @Produce json
// @Param order body service.OrderItem true "OrderItem ID"
// @Success 201 {object} service.OrderItem
// @Failure 400 {object} errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders/items [post]
func (a *Api) createOrderItem(c *gin.Context) {
var orderItem service.OrderItem
err := c.ShouldBindJSON(&orderItem)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
err = service.CreateOrderItem(&orderItem)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusCreated, orderItem)
}
}
// @Schemes
// @Summary update a orderItem
// @Description updates a orderItem with provided information
// @Tags orderItems
// @Accept json
// @Produce json
// @Param orderItem body service.OrderItem true "updated OrderItem"
// @Success 200 {object} service.OrderItem
// @Failure 400 {object} errorResponse
// @Failure 404 "Not Found" errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders/items [put]
func (a *Api) updateOrderItem(c *gin.Context) {
var newOrderItem service.OrderItem
err := c.ShouldBindJSON(&newOrderItem)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
oldOrderItem, err := service.DoesOrderItemExist(strconv.Itoa(int(newOrderItem.ID)))
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = service.UpdateOrderItem(&oldOrderItem, &newOrderItem)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, newOrderItem)
}
}
// @Schemes
// @Summary delete an orderItem
// @Description deletes an orderItem from the database
// @Tags orderItems
// @Produce json
// @Param id path int true "OrderItem ID"
// @Success 200 "OK"
// @Failure 404 "Not Found"
// @Failure 500 {object} errorResponse
// @Router /orders/items/{id} [delete]
func (a *Api) deleteOrderItem(c *gin.Context) {
id := c.Param("id")
orderItem, err := service.DoesOrderItemExist(id)
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = service.DeleteOrderItem(&orderItem)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.Status(http.StatusOK)
}
}

54
api/routesTable.go Normal file
View file

@ -0,0 +1,54 @@
package api
import (
"cafe/service"
"net/http"
"github.com/gin-gonic/gin"
)
// @Schemes
// @Summary get all active tables
// @Description gets all active tables as array
// @Tags tables
// @Produce json
// @Success 200 {array} service.Table
// @Router /tables [get]
func (a *Api) getTables(c *gin.Context) {
c.JSON(http.StatusOK, service.GetAllTables())
}
// @Schemes
// @Summary create new table
// @Description creates a new table and returns it
// @Tags tables
// @Accept json
// @Produce json
// @Success 201 {object} service.Table "Table has been created"
// @Failure 500 {object} errorResponse "Cannot create table"
// @Router /tables [post]
func (a *Api) createTable(c *gin.Context) {
table, err := service.CreateNewTable()
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusCreated, table)
}
}
// @Schemes
// @Summary delete the latest table
// @Description deletes the latest table from the database
// @Tags tables
// @Produce json
// @Success 200 "OK"
// @Failure 500 {object} errorResponse "Cannot delete table"
// @Router /tables [delete]
func (a *Api) deleteTable(c *gin.Context) {
err := service.DeleteLatestTable()
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.Status(http.StatusOK)
}
}

60
api/routesUser.go Normal file
View file

@ -0,0 +1,60 @@
package api
import (
"cafe/types"
"cafe/user"
"net/http"
"github.com/gin-gonic/gin"
)
// @Schemes
// @Summary get a user
// @Description gets a user
// @Tags users
// @Produce json
// @Param username path string true "Username"
// @Success 200 {object} user.User
// @Failure 500 {object} errorResponse
// @Router /users/{username} [get]
func (a *Api) getUser(c *gin.Context) {
username := c.Param("username")
u, err := user.GetUserOrCreate(username)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, u)
}
}
// @Schemes
// @Summary update a user
// @Description updates a user with provided information
// @Tags users
// @Accept json
// @Produce json
// @Param user body user.User true "updated User"
// @Success 200 {object} user.User
// @Failure 400 {object} errorResponse
// @Failure 404 "Not Found" errorResponse
// @Failure 500 {object} errorResponse
// @Router /users [put]
func (a *Api) updateUser(c *gin.Context) {
var newUser user.User
err := c.ShouldBindJSON(&newUser)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
oldUser, err := user.DoesUserExist(newUser.Username)
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = user.UpdateUser(&oldUser, &newUser)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, newUser)
}
}

26
api/static.go Normal file
View file

@ -0,0 +1,26 @@
package api
import (
"cafe/config"
"os"
"path/filepath"
"strings"
"github.com/gin-contrib/static"
"github.com/sirupsen/logrus"
)
func (a *Api) HandleStaticFiles() {
a.Router.LoadHTMLFiles(config.TemplatesDir + "index.html")
a.serveFoldersInTemplates()
}
func (a *Api) serveFoldersInTemplates() {
_ = filepath.WalkDir(config.TemplatesDir, func(path string, info os.DirEntry, err error) error {
if info.IsDir() && info.Name() != strings.TrimSuffix(config.TemplatesDir, "/") {
a.Router.Use(static.Serve("/"+info.Name(), static.LocalFile(config.TemplatesDir+info.Name(), false)))
logrus.WithField("folder", info.Name()).Debug("Serve static folder")
}
return err
})
}

31
api/swagger.go Normal file
View file

@ -0,0 +1,31 @@
package api
import (
"cafe/config"
"cafe/docs"
"net/http"
"net/url"
"os"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func (a *Api) SetupSwagger() {
if config.Cafe.Swagger {
docs.SwaggerInfo.Title = "Cafe"
docs.SwaggerInfo.Description = "This is the backend of a cafe"
docs.SwaggerInfo.Version = os.Getenv("VERSION")
docs.SwaggerInfo.BasePath = "/api"
parsed, _ := url.Parse(config.Cafe.AllowedHosts[0])
docs.SwaggerInfo.Host = parsed.Host
a.Router.GET("/swagger", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
})
a.Router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
logrus.WithField("url", config.Cafe.AllowedHosts[0]+"/swagger").Info("Swagger running")
}
}

16
api/types.go Normal file
View file

@ -0,0 +1,16 @@
package api
import (
"cafe/hub"
"github.com/gin-gonic/gin"
)
type Api struct {
Router *gin.Engine
Hub hub.Hub
}
type errorResponse struct {
Error string `json:"error" validate:"required"`
}

10
config.toml Normal file
View file

@ -0,0 +1,10 @@
ALLOWED_HOSTS = "https://cafe.test"
LOG_LEVEL = "info"
PORT = 5000
SWAGGER = true
[MYSQL]
DATABASE = ""
PASSWORD = ""
URL = ""
USER = ""

72
config/config.go Normal file
View file

@ -0,0 +1,72 @@
package config
import (
"cafe/database"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/unjx-de/go-folder"
)
const StorageDir = "storage/"
const TemplatesDir = "templates/"
type Config struct {
Port int
AllowedHosts []string `mapstructure:"ALLOWED_HOSTS"`
Swagger bool
Bookmarks bool
LogLevel string `mapstructure:"LOG_LEVEL"`
Database database.MySQL `mapstructure:"MYSQL"`
}
var Cafe = Config{}
func configLogger() {
logrus.SetFormatter(&logrus.TextFormatter{TimestampFormat: "2006/01/02 15:04:05", FullTimestamp: true})
}
func readConfig() {
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("toml")
err := viper.ReadInConfig()
if err != nil {
logrus.WithField("error", err).Fatal("Failed opening config file")
}
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
err = viper.Unmarshal(&Cafe, viper.DecodeHook(mapstructure.StringToSliceHookFunc(",")))
if err != nil {
logrus.WithField("error", err).Fatal("Failed reading environment variables")
}
logrus.WithField("file", viper.ConfigFileUsed()).Info("Initializing configuration")
}
func setLogLevel() {
logLevel, err := logrus.ParseLevel(Cafe.LogLevel)
if err != nil {
logrus.SetLevel(logrus.InfoLevel)
} else {
logrus.SetLevel(logLevel)
}
logrus.WithField("logLevel", logLevel.String()).Debug("Log level set")
}
func createFolderStructure() {
folders := []string{StorageDir, TemplatesDir}
err := folder.CreateFolders(folders, 0755)
if err != nil {
logrus.WithField("error", err).Fatal("Failed creating folders")
}
logrus.WithField("folders", folders).Debug("Folders created")
}
func init() {
configLogger()
readConfig()
setLogLevel()
createFolderStructure()
}

80
database/database.go Normal file
View file

@ -0,0 +1,80 @@
package database
import (
"fmt"
"net"
"time"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type MySQL struct {
Url string
User string
Password string
Database string
ORM *gorm.DB
}
func (config *MySQL) MigrateHelper(i interface{}, name string) {
err := config.ORM.AutoMigrate(i)
if err != nil {
logrus.WithField("error", err).Fatalf("Failed to migrate %s", name)
}
}
func (config *MySQL) tryDbConnection() {
i := 1
total := 20
for i <= total {
ln, err := net.DialTimeout("tcp", config.Url, 1*time.Second)
if err != nil {
if i == total {
logrus.WithField("attempt", i).Fatal("Failed connecting to database")
}
logrus.WithField("attempt", i).Warning("Connecting to database")
time.Sleep(2 * time.Second)
i++
} else {
_ = ln.Close()
logrus.WithField("attempt", i).Info("Connected to database")
i = total + 1
}
}
}
func (config *MySQL) initializeMySql() {
var err error
config.tryDbConnection()
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
config.User,
config.Password,
config.Url,
config.Database,
)
config.ORM, err = gorm.Open(mysql.Open(dsn), &gorm.Config{PrepareStmt: true})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to open database")
}
}
func (config *MySQL) initializeSqLite(storageDir string) {
var err error
absPath := storageDir + "db.sqlite"
config.ORM, err = gorm.Open(sqlite.Open(absPath), &gorm.Config{PrepareStmt: true})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to open database")
}
}
func (config *MySQL) Initialize(storageDir string) {
if config.Url == "" {
config.initializeSqLite(storageDir)
} else {
config.initializeMySql()
}
logrus.WithField("dialect", config.ORM.Dialector.Name()).Debug("Database initialized")
}

129
docker-compose.yml Normal file
View file

@ -0,0 +1,129 @@
networks:
net:
secrets:
jwt:
file: ./docker/secrets/not_secure
session:
file: ./docker/secrets/not_secure
storage:
file: ./docker/secrets/not_secure
services:
traefik:
image: traefik:2.10
container_name: traefik
restart: unless-stopped
command:
- '--api=true'
- '--api.dashboard=true'
- '--api.insecure=false'
- '--pilot.dashboard=false'
- '--global.sendAnonymousUsage=false'
- '--global.checkNewVersion=false'
- '--providers.docker=true'
- '--providers.docker.exposedByDefault=false'
- '--entryPoints.http=true'
- '--entryPoints.http.address=:80/tcp'
- '--entryPoints.http.http.redirections.entryPoint.to=https'
- '--entryPoints.http.http.redirections.entryPoint.scheme=https'
- '--entryPoints.http.forwardedHeaders.trustedIPs=172.23.0.0/16'
- '--entryPoints.http.proxyProtocol.trustedIPs=172.23.0.0/16'
- '--entryPoints.http.forwardedHeaders.insecure=false'
- '--entryPoints.http.proxyProtocol.insecure=false'
- '--entryPoints.https=true'
- '--entryPoints.https.address=:443/tcp'
- '--entryPoints.https.forwardedHeaders.trustedIPs=172.23.0.0/16'
- '--entryPoints.https.proxyProtocol.trustedIPs=172.23.0.0/16'
- '--entryPoints.https.forwardedHeaders.insecure=false'
- '--entryPoints.https.proxyProtocol.insecure=false'
environment:
- TZ=Europe/Berlin
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- '80:80'
- '443:443'
labels:
- 'traefik.enable=true'
- 'traefik.http.middlewares.authelia.forwardAuth.address=http://authelia:9091/api/verify?rd=https%3A%2F%2Fcafe.test/auth%2F'
- 'traefik.http.middlewares.authelia.forwardAuth.trustForwardHeader=true'
- 'traefik.http.middlewares.authelia.forwardAuth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
- 'traefik.http.routers.api.rule=Host(`proxy.cafe.test`)'
- 'traefik.http.routers.api.entryPoints=https'
- 'traefik.http.routers.api.tls=true'
- 'traefik.http.routers.api.service=api@internal'
networks:
- net
authelia:
image: authelia/authelia:latest
container_name: authelia
secrets:
- jwt
- session
- storage
environment:
- TZ=Europe/Berlin
- AUTHELIA_JWT_SECRET_FILE=/run/secrets/jwt
- AUTHELIA_SESSION_SECRET_FILE=/run/secrets/session
- AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/run/secrets/storage
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.authelia.rule=Host(`cafe.test`) && PathPrefix(`/auth`)'
- 'traefik.http.routers.authelia.entryPoints=https'
- 'traefik.http.routers.authelia.tls=true'
volumes:
- /etc/localtime:/etc/localtime:ro
- ./docker/authelia:/config
healthcheck:
disable: true
expose:
- 9091
networks:
- net
cafe-frontend:
build:
context: .
dockerfile_inline: |
FROM node:18
container_name: cafe-frontend
entrypoint: yarn run serve
working_dir: /app
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.frontend.rule=Host(`cafe.test`)'
- 'traefik.http.routers.frontend.entryPoints=https'
- 'traefik.http.routers.frontend.tls=true'
- 'traefik.http.routers.frontend.middlewares=authelia@docker'
ports:
- '8080:8080'
networks:
- net
volumes:
- ./frontend:/app/
cafe-backend:
build:
context: .
dockerfile: docker/Dockerfile.dev
args:
- GOLANG_VERSION=${GOLANG_VERSION}
container_name: cafe-backend
entrypoint: air --build.exclude_dir "node_modules,frontend,static,docs,storage,tmp,dist"
environment:
- PUID=1000
- PGID=1000
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.backend.rule=Host(`cafe.test`) && PathPrefix(`/api`)'
- 'traefik.http.routers.backend.entryPoints=https'
- 'traefik.http.routers.backend.tls=true'
- 'traefik.http.routers.backend.middlewares=authelia@docker'
expose:
- 5000
networks:
- net
volumes:
- ./:/app/

51
docker/Dockerfile Normal file
View file

@ -0,0 +1,51 @@
ARG GOLANG_VERSION
ARG NODE_VERSION
ARG ALPINE_VERSION
FROM golang:${GOLANG_VERSION}-alpine AS goBuilder
RUN apk add cmake g++ gcc
WORKDIR /app
COPY ./swagger.sh .
RUN ./swagger.sh install
COPY ./go.mod .
COPY ./go.sum .
RUN go mod download
COPY . .
RUN ./swagger.sh init
RUN go build -ldflags="-s -w"
FROM node:${NODE_VERSION}-alpine AS nodeBuilder
WORKDIR /app
COPY ./frontend/package.json .
COPY ./frontend/yarn.lock .
RUN yarn install --frozen-lockfile
COPY --from=goBuilder /app/docs/swagger.json ../docs/swagger.json
COPY ./frontend/ .
RUN yarn run types:openapi
RUN yarn run build
FROM alpine:${ALPINE_VERSION} AS logo
RUN apk add figlet
RUN figlet Cafe > logo.txt
FROM alpine:${ALPINE_VERSION} AS final
RUN apk add tzdata
WORKDIR /app
COPY ./scripts/entrypoint.sh .
COPY --from=logo /logo.txt .
COPY --from=nodeBuilder /app/dist/ ./templates/
COPY --from=goBuilder /app/cafe .
COPY config.toml .
ARG VERSION
ENV VERSION=$VERSION
ARG BUILD_TIME
ENV BUILD_TIME=$BUILD_TIME
ENTRYPOINT ["/app/entrypoint.sh"]

15
docker/Dockerfile.dev Normal file
View file

@ -0,0 +1,15 @@
ARG GOLANG_VERSION
FROM golang:${GOLANG_VERSION}-alpine
RUN apk add cmake g++ gcc
WORKDIR /app
COPY ./go.mod .
COPY ./go.sum .
RUN go mod download
RUN go install github.com/cosmtrek/air@latest
ENV VERSION=v0.0.0-DEV
ENV BUILD_TIME=2023-06-01T08:07:43.454Z
CMD ["air"]

View file

@ -0,0 +1,50 @@
default_redirection_url: http://cafe.test
server:
host: authelia
path: auth
port: 9091
buffers:
read: 8192
write: 8192
log:
level: error
theme: auto
authentication_backend:
password_reset:
disable: true
file:
path: /config/users_database.yml
access_control:
default_policy: deny
rules:
- domain_regex: 'cafe.test'
policy: one_factor
totp:
disable: true
webauthn:
disable: true
session:
name: auth_cafe_plaetschwiesle
domain: cafe.test
regulation:
max_retries: 3
find_time: 2m
ban_time: 5m
storage:
local:
path: /config/db.sqlite3
notifier:
disable_startup_check: false
filesystem:
filename: /config/notification.txt

View file

@ -0,0 +1,14 @@
users:
besitzer:
displayname: 'Besitzer'
password: '$argon2id$v=19$m=65536,t=3,p=4$Qno2VXJTVVNNNERjVkVXbQ$rEUoGFLekVIVXm76ahP8hcqLHjstRpM1pMLf0tUTBJM'
email: mail@example.com
groups:
- account
- serve
bedienung:
displayname: 'Bedienung'
password: '$argon2id$v=19$m=65536,t=3,p=4$WjlhejJVSXc5TVNLQVprUw$i6DzQukeTsXh3VL36KtCyt+rAdbJSG5AMe3c8Xiw34Q'
email: mail@example.com
groups:
- serve

View file

@ -0,0 +1 @@
this_is_not_a_secure_password

2
frontend/.dockerignore Normal file
View file

@ -0,0 +1,2 @@
src/services/openapi/
node_modules/

24
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/src/services/openapi/

29
frontend/README.md Normal file
View file

@ -0,0 +1,29 @@
# frontend
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your unit tests
```
npm run test:unit
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

3
frontend/babel.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

98
frontend/package.json Normal file
View file

@ -0,0 +1,98 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"types:openapi": "openapi -i ../docs/swagger.json -o src/services/openapi",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/vue-fontawesome": "^3.0.1",
"@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0",
"core-js": "^3.8.3",
"moment": "^2.29.3",
"primeflex": "^3.3.0",
"primeicons": "^6.0.1",
"primevue": "^3.23.0",
"vue": "^3.2.13",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.8.0",
"@types/jest": "^29.5.2",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-plugin-unit-jest": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/compiler-dom": "^3.0.1",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/server-renderer": "^3.0.1",
"@vue/test-utils": "^2.0.0-0",
"@vue/vue3-jest": "^29.2.4",
"babel-jest": "^29.5.0",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.15.1",
"jest": "^29.5.0",
"less": "^4.0.0",
"less-loader": "^11.1.3",
"openapi-typescript": "^6.2.8",
"openapi-typescript-codegen": "^0.24.0",
"prettier": "^2.4.1",
"ts-jest": "^29.1.1",
"typescript": ">=3.3.1 <5.1.0",
"webpack": "^5.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended"
],
"parserOptions": {
"ecmaVersion": 2020
},
"rules": {},
"overrides": [
{
"files": [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)"
],
"env": {
"jest": true
}
}
]
},
"prettier": {
"printWidth": 160
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
],
"jest": {
"preset": "@vue/cli-plugin-unit-jest/presets/typescript-and-babel"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,153 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="750.000000pt" height="750.000000pt" viewBox="0 0 750.000000 750.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,750.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1137 7112 c-16 -18 -103 -205 -117 -253 -9 -30 4 -59 26 -59 8 0 33
9 56 21 22 11 44 17 48 12 4 -4 12 -33 19 -63 10 -48 42 -115 83 -171 15 -21
-18 -99 -196 -459 -148 -298 -181 -381 -173 -438 3 -20 21 -122 41 -225 41
-212 56 -383 56 -630 l0 -169 -85 -176 c-48 -97 -99 -193 -114 -212 -15 -19
-36 -51 -46 -70 -27 -53 -133 -151 -202 -187 -77 -42 -138 -44 -215 -8 -64 30
-99 32 -123 5 -25 -28 -13 -82 27 -115 48 -41 236 -131 298 -144 63 -12 112
-4 162 27 92 57 162 92 169 85 11 -11 37 -328 49 -603 27 -623 35 -718 66
-813 9 -27 12 -52 8 -57 -5 -4 -63 -22 -129 -40 -129 -34 -339 -112 -367 -138
-22 -18 -23 -45 -3 -103 8 -24 16 -59 18 -78 6 -59 22 -60 154 -7 293 115 659
181 1147 206 199 10 344 28 400 50 14 5 28 20 32 35 7 27 33 33 78 15 39 -14
54 -3 59 43 2 23 16 123 31 222 14 99 31 223 37 275 21 190 76 428 148 640 98
290 105 321 91 392 -6 32 -8 63 -5 68 10 17 -23 67 -52 78 -15 6 -53 16 -85
22 -71 14 -128 52 -209 139 -68 73 -109 130 -164 223 -45 77 -114 158 -136
158 -20 0 -56 -43 -160 -190 -100 -144 -245 -291 -322 -330 -33 -16 -71 -30
-83 -30 -25 0 -49 -28 -59 -70 -4 -16 7 -67 30 -140 103 -324 199 -953 181
-1183 -7 -94 -35 -164 -68 -171 -13 -3 -70 -10 -128 -16 -58 -7 -150 -20 -205
-30 -55 -10 -103 -16 -108 -13 -4 2 -3 13 3 23 5 10 10 28 10 39 0 11 10 36
21 56 15 26 20 46 17 73 -3 20 -2 96 3 167 4 73 3 242 -3 385 -5 140 -11 368
-12 505 -3 269 -5 281 -63 386 l-27 50 56 112 c94 188 230 488 306 677 62 152
94 229 162 385 29 66 60 140 69 165 9 25 24 59 33 75 9 17 41 91 71 165 31 74
76 180 102 235 25 55 50 109 54 120 18 46 86 179 155 305 78 143 183 279 270
350 75 61 85 57 138 -57 61 -130 108 -288 108 -359 0 -14 13 -68 29 -122 16
-53 31 -117 34 -142 3 -25 13 -65 22 -90 9 -25 27 -97 40 -160 27 -134 105
-414 136 -491 12 -30 34 -90 49 -134 75 -219 250 -554 339 -648 40 -43 127
-90 153 -82 55 15 158 59 158 68 0 5 -32 43 -70 85 -49 52 -83 100 -111 158
-22 45 -44 84 -48 86 -14 5 -159 339 -195 450 -19 59 -40 137 -46 175 -7 37
-24 118 -40 178 -15 61 -31 130 -35 155 -4 25 -19 99 -35 165 -16 66 -32 147
-35 180 -3 33 -12 89 -20 125 -7 36 -16 84 -19 108 -9 61 -63 227 -96 292 -40
77 -83 135 -171 228 -63 66 -84 82 -115 87 -22 3 -54 13 -73 21 -55 24 -120
28 -189 10 -137 -35 -244 -137 -385 -366 -78 -127 -217 -398 -217 -423 0 -9
-4 -18 -9 -21 -5 -3 -20 -34 -34 -68 -14 -35 -36 -88 -50 -118 -41 -92 -104
-241 -238 -568 -142 -345 -146 -355 -158 -348 -4 3 -11 88 -14 188 -7 188 -32
363 -60 431 -16 37 -16 39 7 75 52 79 202 357 302 556 106 212 119 253 89 287
-9 9 -29 68 -46 130 -16 62 -42 136 -58 164 l-29 51 29 32 c16 18 29 37 29 42
0 16 -60 49 -129 70 -78 25 -103 25 -124 2z m936 -2859 c56 -73 228 -243 292
-288 69 -50 79 -67 71 -128 -4 -29 -34 -133 -67 -232 -54 -159 -65 -211 -104
-445 -47 -288 -85 -594 -85 -677 0 -29 -3 -53 -6 -53 -3 0 -33 11 -67 24 -100
38 -128 46 -171 46 -23 0 -57 3 -77 6 l-36 6 5 122 c5 123 -14 335 -43 493 -8
43 -26 143 -40 223 -30 174 -72 334 -120 459 -40 107 -41 111 -18 111 10 0 49
12 88 27 57 21 82 39 135 92 36 37 81 95 100 129 37 67 87 142 94 142 3 0 25
-26 49 -57z"/>
<path d="M1965 3159 c-9 -13 5 -149 17 -172 16 -30 51 -17 65 23 16 46 16 133
1 148 -15 15 -74 16 -83 1z"/>
<path d="M3685 6514 c-16 -2 -73 -9 -125 -15 -97 -11 -292 -49 -390 -75 -97
-27 -180 -55 -180 -62 0 -32 37 -165 47 -169 7 -3 47 7 90 21 135 44 294 77
523 107 119 15 478 12 620 -6 407 -51 792 -184 1123 -389 774 -478 1263 -1259
1357 -2166 13 -127 13 -430 -1 -566 -51 -523 -247 -1025 -566 -1453 -119 -160
-400 -439 -565 -562 -423 -315 -899 -498 -1426 -550 -149 -15 -510 -6 -652 16
-645 100 -1214 398 -1657 869 -100 107 -270 331 -343 451 -56 92 -53 91 -146
74 -79 -14 -94 -19 -94 -30 0 -5 27 -55 61 -110 431 -714 1136 -1225 1928
-1398 241 -53 360 -65 646 -66 265 0 352 7 555 46 489 94 928 296 1320 608
181 145 410 386 557 586 720 983 776 2345 140 3391 -137 226 -284 409 -489
609 -386 378 -855 638 -1382 765 -237 58 -357 72 -651 75 -148 2 -283 1 -300
-1z"/>
<path d="M2203 5810 c-52 -11 -137 -51 -192 -90 -57 -42 -148 -183 -177 -277
-16 -48 -25 -101 -26 -153 -1 -76 1 -82 40 -148 62 -104 123 -166 200 -204 57
-28 77 -33 142 -33 95 0 135 19 216 101 96 99 143 207 159 364 8 77 -15 160
-75 281 -48 95 -100 143 -169 158 -58 12 -59 12 -118 1z m-51 -192 c10 -29 -3
-113 -19 -125 -29 -22 -103 -10 -103 17 0 11 100 120 110 120 4 0 10 -6 12
-12z m196 -20 c5 -7 13 -41 17 -75 8 -69 3 -73 -50 -53 -21 8 -24 14 -19 37
24 107 30 117 52 91z m-244 -276 c4 -7 -15 -102 -30 -151 -6 -18 -39 11 -60
51 -17 34 -18 95 -1 101 20 9 85 8 91 -1z m241 -1 c8 -14 -31 -110 -56 -137
-32 -34 -49 -31 -49 9 1 47 19 120 33 129 17 11 65 10 72 -1z"/>
<path d="M5872 3183 c-5 -10 -15 -46 -21 -80 -7 -35 -30 -123 -52 -195 -98
-327 -103 -473 -17 -514 25 -12 40 -13 60 -5 32 12 50 41 38 62 -8 12 -13 12
-30 1 -32 -20 -57 -7 -63 33 -12 73 37 294 117 534 47 137 48 160 10 175 -25
9 -33 7 -42 -11z"/>
<path d="M2915 2988 c-85 -30 -261 -190 -340 -309 -96 -144 -128 -252 -89
-307 20 -28 64 -30 64 -3 0 11 -4 23 -10 26 -5 3 -10 27 -10 53 1 133 225 398
405 478 91 40 180 27 222 -32 49 -68 67 -203 38 -284 -24 -68 -97 -169 -191
-263 -62 -61 -85 -90 -77 -98 45 -45 256 175 313 327 28 77 28 209 -1 265 -27
54 -92 123 -133 143 -39 18 -144 20 -191 4z"/>
<path d="M4585 2952 c-6 -4 -18 -32 -29 -62 -10 -30 -26 -73 -36 -95 -28 -62
-120 -314 -120 -328 0 -44 -47 -136 -98 -195 -143 -164 -163 -176 -190 -120
-23 48 25 205 77 252 20 18 21 18 30 -7 17 -45 49 -46 70 -1 35 77 -20 156
-91 132 -52 -18 -118 -123 -152 -240 -9 -32 -20 -58 -24 -58 -4 0 -31 24 -60
53 -52 53 -65 90 -41 114 18 18 2 43 -26 43 -35 0 -65 -33 -65 -73 0 -33 25
-77 95 -167 51 -67 45 -103 -21 -116 -40 -7 -63 13 -69 61 -3 22 -10 44 -16
50 -16 16 -39 -23 -39 -67 0 -75 89 -137 166 -116 30 7 86 60 96 89 3 8 17 3
46 -16 33 -21 49 -26 75 -21 37 7 99 66 138 131 l24 40 3 -57 c2 -31 9 -61 16
-67 22 -18 44 -2 50 37 9 52 102 234 142 279 41 47 43 33 12 -72 -20 -67 -21
-80 -10 -122 16 -57 28 -73 55 -73 18 0 19 4 12 53 -12 83 -7 123 30 234 19
57 35 112 35 123 0 27 -36 45 -64 32 -19 -8 -112 -123 -153 -188 -23 -36 -14
16 13 81 14 33 41 101 61 150 19 50 46 114 59 143 13 29 24 56 24 61 0 5 7 14
16 21 8 7 14 24 12 38 -3 27 -37 55 -53 44z"/>
<path d="M5166 2904 c-24 -23 -18 -41 18 -61 47 -26 78 24 38 60 -23 21 -36
21 -56 1z"/>
<path d="M6054 2881 c-94 -57 -191 -270 -168 -368 27 -113 114 -159 210 -110
72 37 140 123 151 192 8 47 -12 45 -42 -5 -34 -57 -99 -116 -149 -135 -47 -18
-47 -18 -71 5 -20 19 -25 33 -25 72 0 27 7 64 15 84 13 33 16 34 53 28 57 -9
96 15 128 79 36 69 38 132 5 158 -29 24 -67 24 -107 0z m30 -128 c-20 -40 -54
-58 -54 -28 0 18 58 81 67 72 3 -3 -3 -23 -13 -44z"/>
<path d="M3906 2858 c-9 -12 -16 -28 -16 -36 0 -20 -46 -106 -66 -123 -9 -8
-51 -23 -93 -35 -68 -18 -76 -23 -79 -46 -5 -40 27 -53 72 -31 20 9 39 14 42
10 4 -4 -16 -55 -43 -114 -28 -60 -65 -153 -82 -208 -28 -88 -39 -107 -87
-159 -42 -46 -57 -57 -66 -48 -15 15 11 96 53 162 58 94 58 130 -2 130 -18 0
-32 7 -39 19 -14 27 -62 36 -98 17 -64 -33 -210 -213 -253 -313 -36 -84 -21
-151 39 -174 57 -22 99 4 204 130 l27 32 13 -27 c32 -65 80 -57 140 23 l42 57
17 -41 c19 -45 39 -60 84 -65 45 -5 44 25 -1 51 l-37 22 6 72 c4 58 18 104 72
241 39 97 80 182 96 201 27 30 34 33 103 37 48 3 85 11 102 23 26 17 27 18 9
31 -11 8 -43 14 -75 14 -31 0 -59 3 -63 6 -3 4 5 23 18 43 29 41 32 83 9 105
-21 21 -30 20 -48 -6z m-461 -558 c8 -12 -21 -62 -99 -172 -95 -135 -146 -179
-146 -128 0 46 81 171 173 268 41 42 60 51 72 32z"/>
<path d="M5572 2840 c-13 -6 -30 -27 -39 -47 -25 -60 -12 -129 41 -228 52 -96
63 -165 26 -165 -26 0 -43 34 -50 100 -3 30 -9 55 -15 55 -5 0 -19 -16 -32
-35 -56 -86 -71 -107 -115 -153 -29 -30 -54 -47 -65 -45 -29 5 -55 66 -50 117
6 59 23 75 65 60 26 -9 37 -8 57 5 33 22 97 139 102 189 6 55 -24 97 -68 97
-45 0 -127 -87 -172 -181 -30 -64 -41 -102 -63 -215 -5 -28 -74 -98 -86 -86
-14 14 38 228 83 339 16 39 -1 66 -40 61 -22 -3 -26 -9 -37 -73 -15 -90 -44
-195 -55 -195 -4 0 -10 26 -14 58 -20 183 -39 256 -70 268 -9 4 -17 5 -19 3
-2 -2 5 -62 15 -134 20 -142 25 -299 9 -339 -20 -55 -63 -17 -74 66 -9 59 -34
93 -63 84 -12 -4 -25 -17 -30 -29 -14 -38 -88 -167 -95 -167 -23 0 -6 56 65
224 47 112 50 120 36 145 -10 17 -23 26 -39 26 -21 0 -25 -7 -36 -60 -7 -33
-22 -81 -33 -107 -31 -73 -61 -181 -61 -222 0 -63 47 -108 95 -90 17 7 55 75
74 137 7 20 16 37 21 37 5 0 15 -19 22 -42 18 -54 56 -83 110 -83 23 0 43 5
45 11 2 6 9 19 16 28 11 16 15 16 41 2 26 -13 33 -13 57 -1 15 8 41 31 57 51
l30 37 40 -42 c43 -45 80 -53 119 -26 23 16 110 127 120 151 9 25 19 15 34
-33 19 -63 62 -90 114 -73 19 6 44 23 56 38 29 38 20 82 -42 202 -45 87 -50
103 -47 151 3 50 5 54 28 54 36 0 39 37 4 64 -29 23 -38 24 -72 11z m-152
-167 c0 -40 -69 -117 -87 -99 -9 8 10 47 42 84 30 36 45 41 45 15z"/>
<path d="M2963 2683 c-7 -2 -18 -23 -24 -46 -6 -23 -30 -73 -54 -112 -23 -38
-75 -130 -114 -203 -175 -321 -246 -531 -198 -579 28 -28 44 -9 49 59 6 76 72
246 148 382 28 49 50 93 50 97 0 5 20 39 44 76 24 37 67 109 95 160 29 51 56
95 61 98 16 10 11 34 -10 55 -21 21 -26 22 -47 13z"/>
<path d="M3320 2661 c-5 -11 -10 -25 -10 -32 0 -6 -49 -94 -109 -194 -234
-390 -288 -518 -247 -580 19 -28 47 -33 66 -10 10 13 10 19 -4 33 -9 10 -16
28 -16 41 0 42 148 311 294 536 122 187 119 180 88 208 -25 22 -49 21 -62 -2z"/>
<path d="M3396 2551 c-21 -24 -14 -57 15 -66 46 -15 65 44 23 71 -18 13 -23
12 -38 -5z"/>
<path d="M3499 2554 c-17 -20 -1 -48 31 -52 35 -6 46 13 26 42 -19 30 -38 33
-57 10z"/>
<path d="M5709 2096 c-2 -3 -42 -8 -89 -11 -97 -7 -121 -10 -585 -64 -424 -50
-592 -72 -831 -111 -104 -17 -207 -33 -229 -36 -39 -5 -229 -38 -525 -90 -80
-14 -185 -35 -235 -46 -49 -11 -117 -26 -150 -33 -33 -7 -87 -20 -120 -29 -32
-9 -74 -16 -93 -16 -72 0 -94 -41 -45 -80 34 -26 66 -23 173 16 41 15 122 37
180 50 164 36 336 72 435 89 50 9 126 23 170 31 44 8 105 19 135 24 112 17
135 22 153 31 9 6 23 9 30 8 13 -3 71 5 197 26 47 8 121 19 165 25 44 6 100
16 125 21 25 5 124 18 220 29 219 26 389 48 460 59 30 5 91 12 135 15 44 3
132 12 195 20 63 8 144 17 180 21 63 6 157 21 193 31 9 3 15 10 12 15 -6 9
-247 13 -256 5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" sizes="180x180" href="<%= BASE_URL %>favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="<%= BASE_URL %>favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="<%= BASE_URL %>favicon/favicon-16x16.png" />
<link rel="manifest" href="<%= BASE_URL %>favicon/site.webmanifest" />
<link rel="mask-icon" href="<%= BASE_URL %>favicon/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1f2d40" />
<title>Cafe Plätschwiesle</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

91
frontend/src/App.vue Normal file
View file

@ -0,0 +1,91 @@
<template>
<Toast style="width: 90vw" position="bottom-right" group="br" />
<TheNavigation @logout="logout" />
<div class="m-2">
<router-view v-slot="{ Component }">
<Transition mode="out-in">
<component :is="Component" />
</Transition>
</router-view>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import TheNavigation from "@/components/UI/TheNavigation.vue";
import Toast from "primevue/toast";
export default defineComponent({
name: "App",
components: { TheNavigation, Toast },
setup() {
async function logout() {
const response = await fetch("/auth/api/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const result = await response.json();
result.status === "OK" && window.location.reload();
}
return { logout };
},
});
</script>
<style lang="less">
@import "primevue/resources/themes/saga-blue/theme.css";
@import "primevue/resources/themes/vela-blue/theme.css" screen and (prefers-color-scheme: dark);
@media (prefers-color-scheme: light) {
img {
content: url(@/assets/logos/logo.png);
}
}
@media (prefers-color-scheme: dark) {
img {
content: url(@/assets/logos/logo_white.png);
}
}
@font-face {
font-family: "roboto";
src: url(@/assets/fonts/roboto.ttf);
font-display: swap;
}
.p-button.p-button-success:enabled:focus,
.p-button.p-button-danger:enabled:focus,
.p-button.p-button-success:enabled:active,
.p-button.p-button-danger:enabled:active {
box-shadow: none !important;
}
body {
margin: 0;
padding: 0;
background-color: var(--surface-b);
}
html,
body {
font-size: 1.2em;
font-family: "roboto", sans-serif;
}
.p-component {
font-family: "roboto", sans-serif;
}
</style>
<style scoped>
.v-enter-active {
transition: opacity 0.2s ease-in;
}
.v-leave-active {
transition: opacity 0.1s ease-out;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1,107 @@
<template>
<div class="container">
<div class="flex flex-column align-items-center justify-content-center">
<img alt="logo" class="mb-3" style="height: auto; width: 5rem" src="" />
<div class="text-center text-sm">Plätschwiesen 2, 72622 Nürtingen<br />Baden-Württemberg</div>
</div>
<Transition>
<WaveSpinner v-if="isLoading" />
<div v-else>
<div class="flex justify-content-between my-5">
<div>{{ date }}</div>
<div>|</div>
<div class="mb-1">Tisch {{ bill.table_id }}</div>
<div>|</div>
<div>{{ time }}</div>
</div>
<div class="text-lg">
<div v-for="billItem in billItems" :key="billItem.id" class="flex flex-column mb-1">
<div class="flex align-items-center justify-content-between">
<div class="entry white-space-nowrap overflow-hidden">{{ billItem.description }}</div>
<div>{{ convertToEur(billItem.total) }}</div>
</div>
<div v-if="billItem.amount !== 1" class="ml-4 font-italic text-sm">{{ billItem.amount }} x {{ convertToEur(billItem.price) }}</div>
</div>
<div class="flex justify-content-end font-bold mt-5 mb-3">Total: {{ convertToEur(bill.total) }}</div>
</div>
</div>
</Transition>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, PropType, ref } from "vue";
import { BillsService, service_Bill, service_BillItem } from "@/services/openapi";
import { convertToEur } from "@/utils";
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
import moment from "moment";
export default defineComponent({
name: "BillModal",
components: { WaveSpinner },
props: { bill: { type: Object as PropType<service_Bill>, required: true } },
setup(props) {
const isLoading = ref(true);
const billItems = ref<service_BillItem[]>();
const date = computed(() => props.bill.created_at && moment.unix(props.bill.created_at).format("DD.MM.YYYY"));
const time = computed(() => props.bill.created_at && moment.unix(props.bill.created_at).format("HH:mm") + " Uhr");
onMounted(() => {
props.bill.id &&
BillsService.getBillsItems(props.bill.id)
.then((res) => {
billItems.value = res;
})
.finally(() => (isLoading.value = false));
});
return { convertToEur, isLoading, billItems, date, time };
},
});
</script>
<style scoped>
.v-enter-active {
transition: opacity 0.2s ease-in;
}
.v-enter-from {
opacity: 0;
}
.container {
--bs-gutter-x: 0;
--bs-gutter-y: 0;
width: 100%;
padding-right: 0.5rem;
padding-left: 0.5rem;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 576px) {
.container {
max-width: 540px;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
@media (min-width: 1400px) {
.container {
max-width: 1320px;
}
}
.entry:first-child:after {
content: " . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ";
}
</style>

View file

@ -0,0 +1,189 @@
<template>
<BaseCard>
<ConfirmDialog></ConfirmDialog>
<div class="p-card shadow-1 md:p-3">
<DataTable :value="orderItems" dataKey="id" :filters="filters" responsiveLayout="scroll" stripedRows class="p-datatable-sm">
<template #header>
<div class="grid p-fluid align-items-center">
<div class="col-9">
<span class="p-input-icon-left">
<i class="pi pi-search" />
<InputText v-model="filters['global'].value" placeholder="Suchen" @keydown.esc="filters['global'].value = null" />
<span v-if="filters['global'].value !== null" class="leftMiddle styling" @click="filters['global'].value = null">
<i class="pi pi-times"></i>
</span>
</span>
</div>
<div class="col-3 text-right">
<Button :disabled="isDisabled" icon="pi pi-plus" class="p-button-rounded" @click="modal = true" />
</div>
</div>
</template>
<Column field="description">
<template #body="slotProps">
<span class="white-space-nowrap">{{ slotProps.data.description }}</span>
</template>
</Column>
<Column field="price" style="text-align: right">
<template #body="slotProps">{{ convertToEur(slotProps.data.price) }}</template>
</Column>
<Column style="width: 3.5rem">
<template #body="slotProps">
<div class="flex align-items-center justify-content-end">
<div
class="mr-2"
:style="{ color: isDisabled ? 'grey' : 'green', cursor: isDisabled ? 'default' : 'pointer' }"
@click="editOrderItem(slotProps.data)"
>
<i class="pi pi-pencil"></i>
</div>
<div :style="{ color: isDisabled ? 'grey' : 'red', cursor: isDisabled ? 'default' : 'pointer' }" @click="confirmDeleteProduct(slotProps.data)">
<i class="pi pi-trash"></i>
</div>
</div>
</template>
</Column>
<template #empty><div class="mb-1">Keine Einträge</div></template>
</DataTable>
</div>
<Dialog position="top" v-model:visible="modal" :modal="true" :showHeader="false" @hide="resetModal" style="min-width: 50vw">
<div class="p-fluid">
<div class="field mt-5">
<InputText :disabled="isDisabled" id="name" v-model.trim="orderItem.description" required="true" autofocus @keydown.enter="saveOrderItem" />
</div>
<div class="field">
<InputNumber
:disabled="isDisabled"
id="currency-germany"
v-model="orderItem.price"
mode="currency"
currency="EUR"
locale="de-DE"
@keydown.enter="saveOrderItem"
/>
</div>
</div>
<div class="flex justify-content-end">
<Button :disabled="isDisabled" icon="pi pi-times" class="p-button-text p-button-rounded p-button-secondary mr-2" @click="resetModal" />
<Button :loading="isDisabled" icon="pi pi-check" class="p-button-rounded p-button-success" @click="saveOrderItem" />
</div>
</Dialog>
</BaseCard>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue";
import { OrderItemsService, service_OrderItem } from "@/services/openapi";
import InputText from "primevue/inputtext";
import { FilterMatchMode } from "primevue/api";
import DataTable from "primevue/datatable";
import Column from "primevue/column";
import Button from "primevue/button";
import { convertToEur, errorToast } from "@/utils";
import Dialog from "primevue/dialog";
import InputNumber from "primevue/inputnumber";
import { useConfirm } from "primevue/useconfirm";
import ConfirmDialog from "primevue/confirmdialog";
import { useToast } from "primevue/usetoast";
export default defineComponent({
name: "OrderItemList",
// eslint-disable-next-line
components: { BaseCard, InputText, DataTable, Column, Button, Dialog, InputNumber, ConfirmDialog },
props: {
orderItems: { type: Array as PropType<service_OrderItem[]>, default: () => [] },
emptyOrderItem: { type: Object as PropType<service_OrderItem>, default: () => ({}) },
title: { type: String, default: "" },
},
emits: ["orderItemChanged", "orderItemDeleted", "orderItemCreated"],
setup(props, { emit }) {
const isDisabled = ref(false);
const toast = useToast();
const confirm = useConfirm();
const modal = ref(false);
const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
});
const orderItem = ref<service_OrderItem>({ ...props.emptyOrderItem });
function editOrderItem(item: service_OrderItem) {
orderItem.value = { ...item };
modal.value = true;
}
watch(props.emptyOrderItem, () => resetModal());
function saveOrderItem() {
if (isDisabled.value) return;
isDisabled.value = true;
if (orderItem.value.id) {
OrderItemsService.putOrdersItems(orderItem.value)
.then((res) => emit("orderItemChanged", res))
.catch((err) => errorToast(toast, err.body.error))
.finally(() => resetModal());
} else {
OrderItemsService.postOrdersItems(orderItem.value)
.then((res) => emit("orderItemCreated", res))
.finally(() => resetModal());
}
}
function confirmDeleteProduct(item: service_OrderItem) {
if (isDisabled.value) return;
confirm.require({
message: item.description + " löschen?",
header: "Achtung",
position: "top",
accept: () => {
isDisabled.value = true;
item.id &&
OrderItemsService.deleteOrdersItems(item.id)
.then(() => emit("orderItemDeleted", item))
.finally(() => resetModal());
},
});
}
function resetModal() {
modal.value = false;
orderItem.value = { ...props.emptyOrderItem };
isDisabled.value = false;
}
return { filters, convertToEur, editOrderItem, saveOrderItem, confirmDeleteProduct, modal, orderItem, resetModal, isDisabled };
},
});
</script>
<style>
.p-datatable .p-datatable-header,
.p-datatable .p-datatable-footer {
background: transparent !important;
}
.p-datatable .p-datatable-header,
.p-datatable .p-datatable-footer,
.p-datatable .p-datatable-tbody > tr,
.p-datatable .p-datatable-tbody > tr > td {
border-width: 0 !important;
}
.p-datatable-thead {
display: none;
}
</style>
<style scoped>
.styling {
cursor: pointer;
color: gray;
border-radius: 50%;
padding: 0.2rem;
}
.leftMiddle {
position: absolute;
top: 50%;
right: 0;
transform: translate(-50%, -50%);
}
</style>

View file

@ -0,0 +1,53 @@
<template>
<SmallCard bgColor="d" :badgeTwo="badgeTwo">
<template #description>{{ order.order_item.description }}</template>
<template #badgeOne>{{ since }}</template>
<template #badgeTwo>Tisch {{ order.table_id }}</template>
<template #right>
<div class="flex align-items-center">
<Button v-if="!newOrder" :disabled="isDisabled" icon="pi pi-check" class="p-button-rounded p-button-success" @click="$emit('orderDone', order)" />
<TheBadge v-else color="danger">NEU</TheBadge>
</div>
</template>
</SmallCard>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref } from "vue";
import { service_Order, types_ItemType } from "@/services/openapi";
import { convertToEur, getCurrentTimeSince, lessThan15SecondsAgo } from "@/utils";
import Button from "primevue/button";
import moment from "moment";
import TheBadge from "@/components/UI/TheBadge.vue";
import SmallCard from "@/components/UI/SmallCard.vue";
export default defineComponent({
name: "OrderCard",
// eslint-disable-next-line
components: { SmallCard, TheBadge, Button },
props: {
order: { type: Object as PropType<service_Order>, required: true },
isDisabled: { type: Boolean, default: false },
itemType: { type: Number as PropType<types_ItemType>, required: false },
},
emits: ["orderDone"],
setup(props) {
moment.locale("de");
// eslint-disable-next-line
let ticker: any;
const since = ref(getCurrentTimeSince(props.order.updated_at));
const newOrder = ref(lessThan15SecondsAgo(props.order.updated_at));
const badgeTwo = computed(() => props.itemType === types_ItemType.ColdDrink);
onMounted(() => {
ticker = setInterval(() => {
since.value = getCurrentTimeSince(props.order.updated_at);
newOrder.value === true && (newOrder.value = lessThan15SecondsAgo(props.order.updated_at));
}, 1000);
});
onUnmounted(() => ticker && clearInterval(ticker));
return { convertToEur, since, newOrder, badgeTwo };
},
});
</script>

View file

@ -0,0 +1,60 @@
<template>
<div v-if="orders.length !== 0">
<BaseToolbar :icon="detailedItemTypeIcon(itemType)" :title="title || detailedItemTypeString(itemType)" btnIcon="check" @click="checkAllOpenOrders" />
<div class="grid">
<OrderCard
v-for="order in orders"
v-bind:key="order.id"
:order="order"
:isDisabled="isDisabled"
:bigRight="true"
@orderDone="(o) => orderDone(o)"
:itemType="itemType"
/>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, inject, PropType, ref } from "vue";
import { OrdersService, service_Order, types_ItemType } from "@/services/openapi";
import { detailedItemTypeIcon, detailedItemTypeString, errorToast, lessThan15SecondsAgo } from "@/utils";
import OrderCard from "@/components/Orders/OrderCard.vue";
import BaseToolbar from "@/components/UI/BaseToolbar.vue";
import { useToast } from "primevue/usetoast";
import { disabled } from "@/keys";
export default defineComponent({
name: "OrderSection",
components: { OrderCard, BaseToolbar },
props: {
orders: { type: Object as PropType<service_Order[]>, required: true },
icon: { type: String, required: false },
title: { type: String, required: false },
itemType: { type: Number as PropType<types_ItemType>, required: false },
},
emits: ["filterOrders"],
setup(props, { emit }) {
const toast = useToast();
const isDisabled = inject(disabled, ref(false));
const collapseOrders = ref(true);
const collapseIcon = computed(() => (collapseOrders.value ? "chevron-down" : "chevron-up"));
function checkAllOpenOrders() {
props.orders.forEach((order) => {
if (!lessThan15SecondsAgo(order.updated_at)) orderDone(order);
});
}
function orderDone(order: service_Order) {
isDisabled.value = true;
order.is_served = true;
OrdersService.putOrders(order)
.then(() => emit("filterOrders", order.id))
.catch((err) => errorToast(toast, err.body.error))
.finally(() => (isDisabled.value = false));
}
return { detailedItemTypeIcon, detailedItemTypeString, checkAllOpenOrders, orderDone, isDisabled, collapseIcon, collapseOrders };
},
});
</script>

View file

@ -0,0 +1,25 @@
<template>
<div class="flex align-items-center">
<div @click="!isDisabled && $emit('decrementOrder')" :style="{ color: isDisabled ? 'grey' : 'red' }" style="cursor: pointer">
<i class="pi pi-minus"></i>
</div>
<div class="mx-2 font-bold">{{ order.order_count }}</div>
<div @click="!isDisabled && $emit('incrementOrder')" :style="{ color: isDisabled ? 'grey' : 'green' }" style="cursor: pointer">
<i class="pi pi-plus"></i>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { service_Order } from "@/services/openapi";
export default defineComponent({
name: "OrderAmountChange",
props: {
order: { type: Object as PropType<service_Order>, required: true },
isDisabled: { type: Boolean, default: false },
},
emits: ["incrementOrder", "decrementOrder"],
});
</script>

View file

@ -0,0 +1,48 @@
<template>
<div>
<BaseToolbar :title="generalItemTypeString(type)" :icon="generalItemTypeIcon(type)" @click="$emit('openModal', type)" btnIcon="plus" />
<div class="grid">
<TableOrderCard v-for="order in OrdersForType" v-bind:key="order.id" :order="order">
<div class="flex align-items-end">
<OrderAmountChange :order="order" :isDisabled="isLoading" @incrementOrder="incrementOrder(order)" @decrementOrder="decrementOrder(order)" />
</div>
</TableOrderCard>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, inject, PropType, ref } from "vue";
import { OrdersService, service_Order } from "@/services/openapi";
import { convertToEur, generalItemTypeString, generalItemTypeIcon } from "@/utils";
import BaseToolbar from "@/components/UI/BaseToolbar.vue";
import TableOrderCard from "@/components/Tables/TableOrderCard.vue";
import OrderAmountChange from "@/components/Tables/OrderAmountChange.vue";
import { loading } from "@/keys";
export default defineComponent({
name: "OverviewPerType",
components: { TableOrderCard, BaseToolbar, OrderAmountChange },
props: {
orders: { type: Array as PropType<service_Order[]>, default: () => [] },
type: { type: Array as PropType<number[]>, required: true },
},
emits: ["openModal", "getData"],
setup(props, { emit }) {
const OrdersForType = computed(() => props.orders.filter((order) => props.type.includes(order.order_item.item_type)));
const isLoading = inject(loading, ref(false));
function incrementOrder(order: service_Order) {
isLoading.value = true;
OrdersService.postOrders(order.order_item_id, order.table_id).finally(() => emit("getData"));
}
function decrementOrder(order: service_Order) {
isLoading.value = true;
OrdersService.deleteOrders(order.order_item_id, order.table_id).finally(() => emit("getData"));
}
return { OrdersForType, isLoading, convertToEur, incrementOrder, decrementOrder, generalItemTypeIcon, generalItemTypeString };
},
});
</script>

View file

@ -0,0 +1,48 @@
<template>
<SmallCard bgColor="a" :to="'/tables/' + table.id">
<template #description>Tisch {{ table.id }}</template>
<template #badge>{{ since }}</template>
<template #right>
<div class="flex align-items-end">
<TheBadge v-if="table.order_count" class="topRight">{{ table.order_count }}</TheBadge>
<div v-if="table.total" class="font-bold">{{ convertToEur(table.total) }}</div>
</div>
</template>
</SmallCard>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, PropType, ref } from "vue";
import { service_Table } from "@/services/openapi";
import moment from "moment";
import { convertToEur, getCurrentTimeSince } from "@/utils";
import TheBadge from "@/components/UI/TheBadge.vue";
import SmallCard from "@/components/UI/SmallCard.vue";
export default defineComponent({
name: "TableCard",
components: { TheBadge, SmallCard },
props: { table: { type: Object as PropType<service_Table>, required: true } },
setup(props) {
moment.locale("de");
// eslint-disable-next-line
let ticker: any;
const since = ref(getCurrentTimeSince(props.table.updated_at));
onMounted(() => {
ticker = setInterval(() => {
since.value = getCurrentTimeSince(props.table.updated_at);
}, 1000);
});
onUnmounted(() => ticker && clearInterval(ticker));
return { since, convertToEur };
},
});
</script>
<style scoped>
.topRight {
position: absolute;
top: -0.2rem;
right: 0.3rem;
}
</style>

View file

@ -0,0 +1,30 @@
<template>
<SmallCard bgColor="d" :badgeTwo="order.total !== order.order_item.price">
<template #description>{{ order.order_item.description }}</template>
<template #badgeOne>{{ convertToEur(order.order_item.price) }}</template>
<template #badgeTwo>{{ convertToEur(order.total) }}</template>
<template #right>
<slot></slot>
</template>
</SmallCard>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue";
import { service_Order, types_ItemType } from "@/services/openapi";
import { convertToEur } from "@/utils";
import SmallCard from "@/components/UI/SmallCard.vue";
export default defineComponent({
name: "TableOrderCard",
components: { SmallCard },
props: {
order: { type: Object as PropType<service_Order>, required: true },
},
emits: ["decrementOrder", "incrementOrder"],
setup(props) {
const showTotal = computed(() => props.order.order_item.price !== props.order.total);
return { convertToEur, types_ItemType, showTotal };
},
});
</script>

View file

@ -0,0 +1,172 @@
<template>
<BaseCard>
<Transition>
<WaveSpinner v-if="initialLoading" />
<div v-else>
<OverviewPerType :type="[types_ItemType.Food]" :orders="orders" @getData="getData" @openModal="(t) => addBeverage(t)" />
<OverviewPerType :type="[types_ItemType.ColdDrink, types_ItemType.HotDrink]" :orders="orders" @getData="getData" @openModal="(t) => addBeverage(t)" />
<div class="h-4rem"></div>
<BottomNavigation>
<template #left>
<router-link :to="{ name: 'Tables' }" class="no-underline">
<Button :disabled="isLoading" icon="pi pi-arrow-left" class="p-button-rounded" />
</router-link>
</template>
<template #middle>
<div class="flex flex-column align-items-center">
<div class="text-sm">Tisch {{ table }}</div>
<div class="font-bold">{{ convertToEur(total) }}</div>
</div>
</template>
<template #right>
<router-link
:style="{ cursor: isLoading || orders.length === 0 ? 'default' : 'pointer' }"
:to="{ name: isLoading || orders.length === 0 ? 'TableDetail' : 'Checkout' }"
class="no-underline"
>
<Button :disabled="isLoading || orders.length === 0" icon="pi pi-money-bill" class="p-button-danger p-button-rounded" />
</router-link>
</template>
</BottomNavigation>
</div>
</Transition>
<Sidebar v-model:visible="newOrderModal" :baseZIndex="10000" position="full">
<div class="p-fluid">
<Listbox
v-model="selected"
:options="options"
:filter="true"
optionLabel="description"
dataKey="id"
optionValue="id"
listStyle="max-height:65vh"
filterPlaceholder="Suchen"
/>
</div>
<div class="flex justify-content-end mt-4">
<Button :loading="isLoading" label="Speichern" icon="pi pi-check" class="p-button p-button-success mr-3" @click="postOrder" />
</div>
</Sidebar>
</BaseCard>
</template>
<script lang="ts">
import { computed, defineComponent, provide, ref } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue";
import { useStore } from "vuex";
import { OrdersService, service_Order, service_OrderItem, types_ItemType } from "@/services/openapi";
import BottomNavigation from "@/components/UI/BottomNavigation.vue";
import Button from "primevue/button";
import { convertToEur } from "@/utils";
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
import Sidebar from "primevue/sidebar";
import Listbox from "primevue/listbox";
import OverviewPerType from "@/components/Tables/OverviewPerType.vue";
import { loading } from "@/keys";
export default defineComponent({
name: "TableOverview",
// eslint-disable-next-line
components: { OverviewPerType, WaveSpinner, BottomNavigation, BaseCard, Button, Sidebar, Listbox },
props: { id: { type: String, default: "0" } },
setup(props) {
const initialLoading = ref(false);
const isLoading = ref(false);
provide(loading, isLoading);
const newOrderModal = ref(false);
const store = useStore();
const selectedOrder = ref();
const table = computed(() => parseInt(props.id));
const total = ref(0);
const orderItems = computed(() => store.getters.getOrderItems);
const options = ref();
const orders = ref<service_Order[]>([]);
store.dispatch("getAllOrderItems");
getData(true);
function getData(initial = false) {
initial && (initialLoading.value = true);
OrdersService.getOrders(table.value, true)
.then((res) => (orders.value = res))
.finally(() => {
updateTotal();
resetValues();
});
}
function resetValues() {
newOrderModal.value = false;
selectedOrder.value = undefined;
isLoading.value = false;
initialLoading.value = false;
}
function updateTotal() {
let temp = 0;
orders.value.forEach((order) => (temp += order.total));
total.value = temp;
}
function addBeverage(itemType: types_ItemType[]) {
newOrderModal.value = true;
options.value = [];
itemType.forEach((type) => {
options.value = options.value.concat(orderItems.value.get(type));
});
options.value.sort((a: service_OrderItem, b: service_OrderItem) => {
const x = a.description.toLowerCase();
const y = b.description.toLowerCase();
if (x < y) return -1;
if (x > y) return 1;
return 0;
});
}
function postOrder() {
isLoading.value = true;
if (selectedOrder.value) {
OrdersService.postOrders(selectedOrder.value, table.value).finally(() => getData());
} else isLoading.value = false;
}
return {
initialLoading,
isLoading,
newOrderModal,
selected: selectedOrder,
options,
table,
total,
convertToEur,
addBeverage,
types_ItemType,
postOrder,
orders,
getData,
};
},
});
</script>
<style scoped>
.v-enter-active {
transition: opacity 0.2s ease-in;
}
.v-enter-from {
opacity: 0;
}
</style>
<style>
.p-sidebar-content {
margin: 0 !important;
padding: 0 !important;
}
.p-listbox {
border: 0 !important;
}
</style>

View file

@ -0,0 +1,50 @@
<template>
<div class="container">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "BaseCard",
});
</script>
<style scoped>
.container {
--bs-gutter-x: 0;
--bs-gutter-y: 0;
width: 100%;
padding-right: calc(var(--bs-gutter-x) * 0.5);
padding-left: calc(var(--bs-gutter-x) * 0.5);
margin-right: auto;
margin-left: auto;
}
@media (min-width: 576px) {
.container {
max-width: 540px;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
@media (min-width: 1400px) {
.container {
max-width: 1320px;
}
}
</style>

View file

@ -0,0 +1,17 @@
<template>
<div class="p-card p-2 shadow-1" :style="`color: var(--text-color); background-color: var(--surface-${bgColor})`" :class="`pr-${paddingRight}`">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "BaseItem",
props: {
paddingRight: { type: String, default: "2" },
bgColor: { type: String, default: "a" },
},
});
</script>

View file

@ -0,0 +1,43 @@
<template>
<Toolbar class="border-0 shadow-1 my-2 p-2 bg-color">
<template #start>
<div class="flex align-items-center">
<div class="font-bold text-2xl">{{ title }}</div>
<div v-if="icon" class="ml-3"><font-awesome-icon :icon="icon" style="font-size: 1.5rem"></font-awesome-icon></div>
</div>
</template>
<template #end>
<Button v-if="btnIcon" :disabled="isLoading" :icon="'pi pi-' + btnIcon" class="p-button-success p-button-rounded" @click="$emit('click')" />
</template>
</Toolbar>
</template>
<script lang="ts">
import { defineComponent, inject, ref } from "vue";
import Toolbar from "primevue/toolbar";
import Button from "primevue/button";
import { loading } from "@/keys";
export default defineComponent({
name: "BaseToolbar",
// eslint-disable-next-line
components: { Toolbar, Button },
emits: ["click"],
props: {
title: { type: String, default: "" },
icon: { type: String, default: "" },
btnIcon: { type: String, default: "" },
},
setup() {
const isLoading = inject(loading, ref(false));
return { isLoading };
},
});
</script>
<style scoped>
.bg-color {
background-color: var(--surface-a);
color: var(--text-color);
}
</style>

View file

@ -0,0 +1,33 @@
<template>
<div class="fixed-bottom">
<div class="flex justify-content-between align-items-center border-round-xs py-2 px-3 bg-color shadow-1">
<div><slot name="left"></slot></div>
<div><slot name="middle"></slot></div>
<div><slot name="right"></slot></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "BottomNavigation",
setup() {
return {};
},
});
</script>
<style scoped>
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
}
.bg-color {
background-color: var(--surface-a);
color: var(--text-color);
}
</style>

View file

@ -0,0 +1,60 @@
<template>
<div v-if="!to && !bigRight" class="col-12 lg:col-6">
<BaseItem class="relative" :bgColor="bgColor">
<div class="flex flex-column justify-content-between">
<div class="white-space-nowrap font-bold overflow-hidden text-overflow-ellipsis"><slot name="description"></slot></div>
<div class="flex justify-content-between">
<div class="flex align-items-center mt-1">
<TheBadge size="sm" color="info"><slot name="badgeOne"></slot></TheBadge>
<TheBadge v-if="badgeTwo" size="sm" color="warning" class="ml-2"><slot name="badgeTwo"></slot></TheBadge>
</div>
<slot name="right"></slot>
</div>
</div>
</BaseItem>
</div>
<div v-else-if="bigRight" class="col-12 lg:col-6">
<BaseItem class="relative" :bgColor="bgColor">
<div class="flex justify-content-between">
<div class="flex flex-column overflow-hidden">
<div class="white-space-nowrap font-bold overflow-hidden text-overflow-ellipsis"><slot name="description"></slot></div>
<div class="flex align-items-center mt-1">
<TheBadge size="sm" color="info"><slot name="badgeOne"></slot></TheBadge>
<TheBadge v-if="badgeTwo" size="sm" color="warning" class="ml-2"><slot name="badgeTwo"></slot></TheBadge>
</div>
</div>
<slot name="right"></slot>
</div>
</BaseItem>
</div>
<router-link v-else class="col-12 lg:col-6 no-underline" :to="to">
<BaseItem class="relative" :bgColor="bgColor">
<div class="flex justify-content-between overflow-hidden">
<div class="flex flex-column align-items-start">
<div class="white-space-nowrap overflow-hidden text-overflow-ellipsis font-bold"><slot name="description"></slot></div>
<div class="flex align-items-center mt-1">
<TheBadge size="sm" color="success"><slot name="badge"></slot></TheBadge>
</div>
</div>
<slot name="right"></slot>
</div>
</BaseItem>
</router-link>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import TheBadge from "@/components/UI/TheBadge.vue";
import BaseItem from "@/components/UI/BaseItem.vue";
export default defineComponent({
name: "SmallCard",
components: { TheBadge, BaseItem },
props: {
bgColor: { type: String, default: "a" },
to: { type: String, default: "" },
badgeTwo: { type: Boolean, default: true },
bigRight: { type: Boolean, default: false },
},
});
</script>

View file

@ -0,0 +1,36 @@
<template>
<div class="badge p-badge" :class="`p-badge-${color}`">
<span :class="`text-${size}`"><slot></slot></span>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "TheBadge",
props: { color: { type: String, default: "primary" }, size: { type: String, default: "xs" } },
});
</script>
<style scoped>
.badge {
--bs-badge-padding-x: 0.5em;
--bs-badge-padding-y: 0.3em;
--bs-badge-font-size: 0.75em;
--bs-badge-font-weight: 700;
--bs-badge-border-radius: 0.375rem;
display: inline-block;
padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);
font-size: var(--bs-badge-font-size);
font-weight: var(--bs-badge-font-weight);
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: var(--bs-badge-border-radius, 0);
}
.badge:empty {
display: none;
}
</style>

View file

@ -0,0 +1,109 @@
<template>
<UserSettings />
<Menubar :model="items" class="py-1 px-3 mb-3 shadow-1 border-0 bg-color">
<template #start>
<router-link class="no-underline" to="/tables"><img alt="logo" class="mr-2" width="50" height="50" src="@/assets/logos/logo.png" /></router-link>
</template>
<template #end>
<div v-if="tablePath">
<Button v-if="tablesCount !== 0" :disabled="isLoading" icon="pi pi-minus" class="p-button-danger p-button-rounded mr-2" @click="removeTable" />
<Button :disabled="isLoading" icon="pi pi-plus" class="p-button-success p-button-rounded" @click="addTable" />
</div>
<router-link v-else :to="{ name: 'Tables' }" class="no-underline">
<Button label="Tische" class="p-button-secondary" icon="pi pi-table" />
</router-link>
</template>
</Menubar>
</template>
<script lang="ts">
import { computed, defineComponent, provide, ref } from "vue";
import Menubar from "primevue/menubar";
import { useStore } from "vuex";
import Button from "primevue/button";
import { useRoute } from "vue-router";
import { TablesService, types_ItemType } from "@/services/openapi";
import { detailedItemTypeString, errorToast } from "@/utils";
import { useToast } from "primevue/usetoast";
import { visible } from "@/keys";
import UserSettings from "@/components/User/UserSettings.vue";
import { MenuItem } from "primevue/menuitem";
export default defineComponent({
name: "TheNavigation",
// eslint-disable-next-line
components: { UserSettings, Menubar, Button },
emits: ["logout"],
setup(_, { emit }) {
const toast = useToast();
const store = useStore();
const route = useRoute();
const isLoading = ref(false);
const tablesCount = computed(() => store.getters.getTablesCount);
const tablePath = computed(() => route.path === "/tables");
const user = computed<string>(() => store.getters.getUsername);
const settingsVisible = ref(false);
provide(visible, settingsVisible);
function removeTable() {
isLoading.value = true;
TablesService.deleteTables()
.then(() => {
store.dispatch("removeLastTable");
})
.catch((err) => {
errorToast(toast, err.body.error);
})
.finally(() => {
isLoading.value = false;
});
}
function addTable() {
isLoading.value = true;
TablesService.postTables()
.then((res) => {
store.dispatch("addTable", res);
})
.catch((err) => {
errorToast(toast, err.body.error);
})
.finally(() => {
isLoading.value = false;
});
}
const items = ref<MenuItem[]>([
{ label: "Bestellungen", icon: "pi pi-fw pi-history", to: "/orders" },
{
label: "Artikel",
icon: "pi pi-fw pi-shopping-cart",
items: [
{ label: detailedItemTypeString(types_ItemType.Food), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.Food },
{ label: detailedItemTypeString(types_ItemType.ColdDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.ColdDrink },
{ label: detailedItemTypeString(types_ItemType.HotDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.HotDrink },
],
},
{ label: "Rechnungen", icon: "pi pi-fw pi-euro", to: "/bills", visible: () => store.getters.getGroups.includes("account") },
{ separator: true },
{
label: user.value,
icon: "pi pi-fw pi-user",
items: [
{ label: "Einstellungen", icon: "pi pi-fw pi-cog", command: () => (settingsVisible.value = true) },
{ label: "Abmelden", icon: "pi pi-fw pi-power-off", command: () => emit("logout") },
],
},
]);
return { items, tablePath, removeTable, addTable, isLoading, tablesCount };
},
});
</script>
<style scoped>
.bg-color {
background-color: var(--surface-a);
color: var(--text-color);
}
</style>

View file

@ -0,0 +1,87 @@
<template>
<div v-if="isShowing" class="center">
<div class="lds-ellipsis">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "WaveSpinner",
setup() {
const isShowing = ref(false);
setTimeout(() => (isShowing.value = true), 200);
return { isShowing };
},
});
</script>
<style scoped>
.center {
display: flex;
justify-content: center;
width: 100%;
}
.lds-ellipsis {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ellipsis div {
position: absolute;
top: 33px;
width: 13px;
height: 13px;
border-radius: 50%;
background: var(--primary-color);
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 8px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 8px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 32px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 56px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(24px, 0);
}
}
</style>

View file

@ -0,0 +1,48 @@
<template>
<Sidebar v-model:visible="isVisible" :baseZIndex="10000" class="pl-3">
<div class="font-bold mb-3">Bestellungen anzeigen:</div>
<div class="flex align-items-center mb-3">
<InputSwitch :disabled="isLoading" id="show_cold_drinks" v-model="user.show_cold_drinks" @change="updateUser()" />
<label for="show_cold_drinks" class="ml-3">Kaltgetränke</label>
</div>
<div class="flex align-items-center">
<InputSwitch :disabled="isLoading" id="show_hot_drinks" v-model="user.show_hot_drinks" @change="updateUser()" />
<label for="show_hot_drinks" class="ml-3">Heiß/Eiskaffee & Speisen</label>
</div>
</Sidebar>
</template>
<script lang="ts">
import { computed, defineComponent, inject, ref } from "vue";
import Sidebar from "primevue/sidebar";
import { visible } from "@/keys";
import { user_User, UsersService } from "@/services/openapi";
import InputSwitch from "primevue/inputswitch";
import { useStore } from "vuex";
import { errorToast } from "@/utils";
import { useToast } from "primevue/usetoast";
export default defineComponent({
name: "UserSettings",
components: { Sidebar, InputSwitch },
setup() {
const store = useStore();
const toast = useToast();
const isLoading = ref(false);
const isVisible = inject(visible, ref(false));
const user = computed<user_User>(() => store.getters.getUser);
function updateUser() {
isLoading.value = true;
UsersService.putUsers(user.value)
.then((res) => store.commit("setUser", res))
.catch((err) => errorToast(toast, err.body.error))
.finally(() => (isLoading.value = false));
}
return { isVisible, user, updateUser, isLoading };
},
});
</script>
<style scoped></style>

5
frontend/src/keys.ts Normal file
View file

@ -0,0 +1,5 @@
import { InjectionKey, Ref } from "vue";
export const loading = Symbol() as InjectionKey<Ref<boolean>>;
export const disabled = Symbol() as InjectionKey<Ref<boolean>>;
export const visible = Symbol() as InjectionKey<Ref<boolean>>;

138
frontend/src/main.ts Normal file
View file

@ -0,0 +1,138 @@
import { createApp } from "vue";
import { OpenAPI, UsersService } from "@/services/openapi";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import PrimeVue from "primevue/config";
import ConfirmationService from "primevue/confirmationservice";
import ToastService from "primevue/toastservice";
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faMugHot } from "@fortawesome/free-solid-svg-icons";
import { faChampagneGlasses } from "@fortawesome/free-solid-svg-icons";
import { faCheese } from "@fortawesome/free-solid-svg-icons";
library.add(faMugHot, faChampagneGlasses, faCheese);
import "primevue/resources/primevue.min.css";
import "primeicons/primeicons.css";
import "primeflex/primeflex.css";
export const API_ENDPOINT_URL = window.origin + "/api";
export const WEBSOCKET_ENDPOINT_URL = API_ENDPOINT_URL.replace("http", "ws") + "/orders/ws";
OpenAPI.BASE = API_ENDPOINT_URL;
async function getHealth() {
const response = await fetch(API_ENDPOINT_URL + "/health");
const groups = response.headers.get("remote-groups")?.split(",");
store.commit("setGroups", groups);
const user = await UsersService.getUsers(response.headers.get("remote-name") || "Benutzer");
store.commit("setUser", user);
}
getHealth().then(() => {
const app = createApp(App);
app.use(store);
app.use(router);
app.use(PrimeVue, {
locale: {
startsWith: "Beginnt mit",
contains: "enthält",
notContains: "enthält nicht",
endsWith: "endet mit",
equals: "entspricht",
notEquals: "entspricht nicht",
noFilter: "Kein Filter",
lt: "Weniger als",
lte: "Weniger als oder gleich viel",
gt: "Mehr als",
gte: "Mehr als oder gleich viel",
dateIs: "Datum ist",
dateIsNot: "Datum ist nicht",
dateBefore: "Datum liegt vor",
dateAfter: "Datum liegt nach",
clear: "Löschen",
apply: "Anwenden",
matchAll: "Alle abgleichen",
matchAny: "Mit jedem abgleichen",
addRule: "Regel hinzufügen",
removeRule: "Regel entfernen",
accept: "Ja",
reject: "Nein",
choose: "Auswählen",
upload: "Hochladen",
cancel: "Abbrechen",
dayNames: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
dayNamesShort: ["Son", "Mon", "Die", "Mit", "Don", "Fre", "Sam"],
dayNamesMin: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
monthNames: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
monthNamesShort: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"],
today: "Heute",
weekHeader: "Wo",
firstDayOfWeek: 1,
dateFormat: "dd.mm.yy",
weak: "Schwach",
medium: "Medium",
strong: "Stark",
passwordPrompt: "Passwort eingeben",
emptySearchMessage: "Keine Ergebnisse gefunden",
emptyMessage: "Keine verfügbaren Optionen",
aria: {
trueLabel: "True",
falseLabel: "False",
nullLabel: "Nicht ausgewählt",
star: "1 Stern",
stars: "{star} Sterne",
selectAll: "Alle Artikel ausgewählt",
unselectAll: "Alle Artikel nicht ausgewählt",
close: "Schließen",
previous: "Vorherig",
next: "Nächste",
navigation: "Navigation",
scrollTop: "Nach Oben scrollen",
moveTop: "Nach oben bewegen",
moveUp: "Aufsteigen",
moveDown: "Absteigen",
moveBottom: "Nach unten bewegen",
moveToTarget: "Move to Target",
moveToSource: "Move to Source",
moveAllToTarget: "Move All to Target",
moveAllToSource: "Move All to Source",
pageLabel: "{page}",
firstPageLabel: "Erste Seite",
lastPageLabel: "Letzte Seite",
nextPageLabel: "Nächste Seite",
prevPageLabel: "Vorherige Seite",
rowsPerPageLabel: "Reihen pro Seite",
jumpToPageDropdownLabel: "Jump to Page Dropdown",
jumpToPageInputLabel: "Jump to Page Input",
selectRow: "Reihe ausgewählt",
unselectRow: "Reihe abgewählt",
expandRow: "Row Expanded",
collapseRow: "Row Collapsed",
showFilterMenu: "Show Filter Menu",
hideFilterMenu: "Hide Filter Menu",
filterOperator: "Filter Operator",
filterConstraint: "Filter Constraint",
editRow: "Row Edit",
saveEdit: "Save Edit",
cancelEdit: "Cancel Edit",
listView: "List View",
gridView: "Grid View",
slide: "Slide",
slideNumber: "{slideNumber}",
zoomImage: "Zoom Image",
zoomIn: "Zoom In",
zoomOut: "Zoom Out",
rotateRight: "Rotate Right",
rotateLeft: "Rotate Left",
},
},
});
app.use(ConfirmationService);
app.use(ToastService);
app.component("font-awesome-icon", FontAwesomeIcon);
router.isReady().then(() => {
app.mount("#app");
});
});

View file

@ -0,0 +1,33 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import TableView from "@/views/Tables.vue";
import ItemView from "@/views/Items.vue";
import OrderView from "@/views/Orders.vue";
import BillView from "@/views/Bills.vue";
import CheckoutView from "@/views/Checkout.vue";
import TableDetail from "@/components/Tables/TableOverview.vue";
import { useStore } from "vuex";
const routes: Array<RouteRecordRaw> = [
{ path: "/tables", name: "Tables", component: TableView, meta: { needsAuth: true } },
{ path: "/tables/:id", name: "TableDetail", props: true, component: TableDetail, meta: { needsAuth: true } },
{ path: "/tables/:id/checkout", name: "Checkout", props: true, component: CheckoutView, meta: { needsAuth: true } },
{ path: "/orders", name: "Orders", component: OrderView, meta: { needsAuth: true } },
{ path: "/items/:id", name: "Items", props: true, component: ItemView, meta: { needsAuth: true } },
{ path: "/bills", name: "Bills", component: BillView, meta: { needsAuth: true } },
{ path: "/:pathMatch(.*)*", redirect: { name: "Tables" } },
];
const router = createRouter({
routes,
history: createWebHistory(process.env.BASE_URL),
});
router.beforeEach(async (to) => {
const store = useStore();
if (to.name === "Bills") {
if (!store.getters.getGroups.includes("account")) return "/tables";
}
return true;
});
export default router;

6
frontend/src/shims-vue.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View file

@ -0,0 +1,43 @@
import { createStore } from "vuex";
import tableStore from "@/store/tables";
import orderItemStore from "@/store/orderItems";
import { user_User } from "@/services/openapi";
interface AppStateModel {
user: user_User;
groups: string[];
}
export default createStore({
state: {
user: {
username: "",
show_cold_drinks: true,
show_hot_drinks: true,
},
groups: [""],
},
getters: {
getUser(state: AppStateModel) {
return state.user;
},
getGroups(state: AppStateModel) {
return state.groups;
},
getUsername(state: AppStateModel) {
return state.user.username;
},
},
mutations: {
setUser(state: AppStateModel, _user: user_User) {
state.user = _user;
},
setGroups(state: AppStateModel, groups: string[]) {
state.groups = groups;
},
},
actions: {},
modules: {
tableStore,
orderItemStore,
},
});

View file

@ -0,0 +1,76 @@
import { OrderItemsService, service_OrderItem, types_ItemType } from "@/services/openapi";
interface AppStateModel {
orderItems: Map<number, service_OrderItem[]>;
}
interface mutationOrderItems {
orderItems: service_OrderItem[];
orderType: types_ItemType;
}
const orderItemStore = {
state: () => ({
orderItems: new Map<number, service_OrderItem[]>(),
}),
getters: {
getOrderItems(state: AppStateModel) {
return state.orderItems;
},
},
mutations: {
setOrderItems(state: AppStateModel, payload: mutationOrderItems) {
state.orderItems.set(payload.orderType, payload.orderItems);
},
pushOrderItem(state: AppStateModel, orderItem: service_OrderItem) {
const tempOrderItems = state.orderItems.get(orderItem.item_type);
tempOrderItems && tempOrderItems.push(orderItem);
},
filterOrderItem(state: AppStateModel, orderItem: service_OrderItem) {
const tempOrderItems = state.orderItems.get(orderItem.item_type);
tempOrderItems &&
state.orderItems.set(
orderItem.item_type,
tempOrderItems.filter((origItem: service_OrderItem) => origItem.id !== orderItem.id)
);
},
putOrderItem(state: AppStateModel, orderItem: service_OrderItem) {
const tempOrderItems = state.orderItems.get(orderItem.item_type);
tempOrderItems &&
state.orderItems.set(
orderItem.item_type,
tempOrderItems.map((origItem: service_OrderItem) => (origItem.id === orderItem.id ? orderItem : origItem))
);
},
},
actions: {
// eslint-disable-next-line
async getAllOrderItems(context: any) {
await context.dispatch("getOrderItems", types_ItemType.Food);
await context.dispatch("getOrderItems", types_ItemType.ColdDrink);
await context.dispatch("getOrderItems", types_ItemType.HotDrink);
},
// eslint-disable-next-line
async getOrderItems(context: any, orderType: types_ItemType) {
const orderTypeArray = context.getters.getOrderItems;
if (!orderTypeArray.get(orderType)) {
const orderItems: service_OrderItem[] | null = await OrderItemsService.getOrdersItems(orderType);
context.commit("setOrderItems", { orderItems, orderType });
}
},
// eslint-disable-next-line
addOrderItem(context: any, orderItem: service_OrderItem) {
context.commit("pushOrderItem", orderItem);
},
// eslint-disable-next-line
deleteOrderItem(context: any, orderItem: service_OrderItem) {
context.commit("filterOrderItem", orderItem);
},
// eslint-disable-next-line
updateOrderItem(context: any, orderItem: service_OrderItem) {
context.commit("putOrderItem", orderItem);
},
},
};
export default orderItemStore;

View file

@ -0,0 +1,47 @@
import { service_Table, TablesService } from "@/services/openapi";
interface AppStateModel {
tables: service_Table[] | null;
}
const tableStore = {
state: () => ({
tables: null,
}),
getters: {
getTables(state: AppStateModel) {
return state.tables;
},
getTablesCount(state: AppStateModel) {
return state.tables && state.tables.length;
},
},
mutations: {
setTables(state: AppStateModel, tables: service_Table[]) {
state.tables = tables;
},
popTables(state: AppStateModel) {
state.tables && state.tables.pop();
},
pushTable(state: AppStateModel, table: service_Table) {
state.tables && state.tables.push(table);
},
},
actions: {
// eslint-disable-next-line
fetchTables(context: any) {
context.commit("setTables", null);
return TablesService.getTables();
},
// eslint-disable-next-line
removeLastTable(context: any) {
context.commit("popTables");
},
// eslint-disable-next-line
addTable(context: any, table: service_Table) {
context.commit("pushTable", table);
},
},
};
export default tableStore;

78
frontend/src/utils.ts Normal file
View file

@ -0,0 +1,78 @@
import { service_Bill, service_Order, types_ItemType } from "@/services/openapi";
export function convertToEur(value: number | undefined) {
const temp: number = value ? value : 0;
return temp.toLocaleString("de-DE", { style: "currency", currency: "EUR" });
}
export function detailedItemTypeString(type: types_ItemType | undefined) {
switch (type) {
case types_ItemType.Food:
return "Speisen";
case types_ItemType.ColdDrink:
return "Kaltgetränke";
default:
return "Heiß/Eiskaffee";
}
}
export function generalItemTypeString(type: types_ItemType[]) {
if (type.includes(types_ItemType.Food)) {
return "Speisen";
} else {
return "Getränke";
}
}
export function detailedItemTypeIcon(type: types_ItemType | undefined) {
switch (type) {
case types_ItemType.Food:
return "fa-cheese";
case types_ItemType.ColdDrink:
return "fa-champagne-glasses";
case types_ItemType.HotDrink:
return "fa-mug-hot";
default:
return "";
}
}
export function generalItemTypeIcon(type: types_ItemType[]) {
if (type.includes(types_ItemType.Food)) {
return "fa-cheese";
} else {
return "fa-champagne-glasses";
}
}
export interface WebSocketMsg {
type: NotifierType;
payload: service_Order[];
}
export enum NotifierType {
Create,
Delete,
DeleteAll,
}
import { ToastServiceMethods } from "primevue/toastservice";
import moment from "moment";
const timeToLife = 3600;
export function errorToast(toast: ToastServiceMethods, message: string) {
toast.removeAllGroups();
toast.add({ severity: "error", summary: "Fehler", detail: message, group: "br", life: timeToLife });
}
export function getCurrentTimeSince(updated_at: number | undefined) {
return updated_at ? moment.unix(updated_at).fromNow() : "";
}
export function lessThan15SecondsAgo(updated_at: number | undefined) {
const updated = updated_at ? moment.unix(updated_at) : moment();
return moment().diff(updated, "seconds") < 15;
}
export const emptyBill: service_Bill = { table_id: 0, total: 0 };

View file

@ -0,0 +1,158 @@
<template>
<BaseCard>
<ConfirmDialog></ConfirmDialog>
<Transition>
<WaveSpinner v-if="isLoading" />
<div v-else class="p-card shadow-1 md:p-3">
<DataTable :value="bills" dataKey="id" :filters="filters" responsiveLayout="scroll" stripedRows class="p-datatable-sm">
<template #header>
<div class="grid p-fluid align-items-center">
<div class="col-12 md:col-4">
<Calendar id="basic" v-model="today" autocomplete="off" :inputStyle="{ 'text-align': 'center' }" :manualInput="false" />
</div>
<div class="col-12 md:col-8">
<span class="p-input-icon-left">
<i class="pi pi-search" />
<InputText v-model="filters['global'].value" placeholder="Suchen" @keydown.esc="filters['global'].value = null" />
<span v-if="filters['global'].value !== null" class="leftMiddle styling" @click="filters['global'].value = null">
<i class="pi pi-times"></i>
</span>
</span>
</div>
</div>
</template>
<Column field="table_id">
<template #body="slotProps">
<span class="white-space-nowrap">
Tisch {{ slotProps.data.table_id }} <span class="text-sm">({{ time(slotProps.data.created_at) }})</span>
</span>
</template>
</Column>
<Column field="total" style="text-align: right">
<template #body="slotProps">{{ convertToEur(slotProps.data.total) }}</template>
</Column>
<Column style="width: 3.5rem">
<template #body="slotProps">
<div class="flex align-items-center justify-content-end">
<div class="mr-2" :style="{ color: isDisabled ? 'grey' : 'green' }" style="cursor: pointer" @click="openBill(slotProps.data.id)">
<i class="pi pi-eye"></i>
</div>
<div :style="{ color: isDisabled ? 'grey' : 'red' }" style="cursor: pointer" @click="deleteBill(slotProps.data.id)">
<i class="pi pi-trash"></i>
</div>
</div>
</template>
</Column>
<template #empty><div class="mb-1">Keine Rechnungen</div></template>
</DataTable>
</div>
</Transition>
<Sidebar v-model:visible="billModal" :baseZIndex="10000" position="full">
<BillModal :bill="bill" />
</Sidebar>
</BaseCard>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue";
import Calendar from "primevue/calendar";
import { BillsService, service_Bill } from "@/services/openapi";
import Sidebar from "primevue/sidebar";
import BillModal from "@/components/Bills/BillModal.vue";
import { convertToEur, emptyBill, errorToast } from "@/utils";
import { FilterMatchMode } from "primevue/api";
import DataTable from "primevue/datatable";
import Column from "primevue/column";
import InputText from "primevue/inputtext";
import moment from "moment";
import { useConfirm } from "primevue/useconfirm";
import { useToast } from "primevue/usetoast";
import ConfirmDialog from "primevue/confirmdialog";
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
export default defineComponent({
name: "BillView",
components: { BillModal, BaseCard, Calendar, Sidebar, DataTable, Column, InputText, ConfirmDialog, WaveSpinner },
setup() {
const confirm = useConfirm();
const toast = useToast();
const today = ref(new Date());
const bills = ref<service_Bill[]>([]);
const isLoading = ref(false);
const isDisabled = ref(false);
const billModal = ref(false);
const bill = ref<service_Bill>({ ...emptyBill });
const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
});
getData();
watch(today, () => getData());
function getData() {
isLoading.value = true;
BillsService.getBills(today.value.getFullYear(), today.value.getUTCMonth() + 1, today.value.getDate())
.then((res) => (bills.value = res))
.catch((err) => errorToast(toast, err.body.error))
.finally(() => {
isLoading.value = false;
});
}
function openBill(billId: number) {
if (isDisabled.value) return;
const temp: service_Bill | undefined = bills.value.find((bill) => bill.id === billId);
temp && (bill.value = temp);
billModal.value = true;
}
function time(unixDate: number) {
return moment.unix(unixDate).format("HH:mm") + " Uhr";
}
function deleteBill(billId: number) {
if (isDisabled.value) return;
confirm.require({
message: "Rechnung löschen?",
header: "Rechnung",
icon: "pi pi-info-circle",
acceptClass: "p-button-danger",
accept: () => {
isDisabled.value = true;
BillsService.deleteBills(billId)
.then(() => (bills.value = bills.value.filter((bill) => bill.id !== billId)))
.catch((err) => errorToast(toast, err.body.error))
.finally(() => (isDisabled.value = false));
},
});
}
return { convertToEur, openBill, deleteBill, today, bills, isLoading, isDisabled, filters, billModal, bill, time };
},
});
</script>
<style scoped>
.v-enter-active {
transition: opacity 0.2s ease-in;
}
.v-enter-from {
opacity: 0;
}
.styling {
cursor: pointer;
color: gray;
border-radius: 50%;
padding: 0.2rem;
}
.leftMiddle {
position: absolute;
top: 50%;
right: 0;
transform: translate(-50%, -50%);
}
</style>

View file

@ -0,0 +1,195 @@
<template>
<BaseCard>
<ConfirmDialog></ConfirmDialog>
<Transition>
<WaveSpinner v-if="isLoading" />
<div v-else>
<BaseItem>
<div class="field-checkbox mt-1">
<Checkbox id="binary" v-model="checkAll" :binary="true" @click="checkAllClicked" />
<label for="binary">Alle Auswählen</label>
</div>
<hr style="color: var(--text-color)" class="my-3" />
<div v-for="order in orders" :key="order.id" class="field-checkbox">
<Checkbox :id="order.id" name="order" :value="order.id" v-model="orderFilter" />
<label :for="'' + order.id" class="w-full">
<span class="flex justify-content-between">
<span class="overflow-hidden text-overflow-ellipsis white-space-nowrap">{{ order.order_item.description }}</span>
<span>{{ convertToEur(order.order_item.price) }}</span>
</span>
</label>
</div>
</BaseItem>
<div class="h-4rem"></div>
<BottomNavigation>
<template #left>
<router-link :to="{ name: 'TableDetail' }" class="no-underline">
<Button :disabled="applyFilterLoading" icon="pi pi-arrow-left" class="p-button-rounded" />
</router-link>
</template>
<template #middle>
<div class="flex flex-column align-items-center">
<div class="text-sm">Tisch {{ table }}</div>
<div class="font-bold">{{ convertToEur(total) }}</div>
</div>
</template>
<template #right>
<Button
:disabled="total === 0"
:loading="applyFilterLoading"
icon="pi pi-money-bill"
class="p-button-danger p-button-rounded"
@click="generateBill"
/>
</template>
</BottomNavigation>
</div>
</Transition>
<Sidebar v-model:visible="billModal" :baseZIndex="10000" position="full" @hide="billModalClosed">
<BillModal :bill="bill" />
</Sidebar>
</BaseCard>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch } from "vue";
import { BillsService, OrdersService, service_Bill, service_Order } from "@/services/openapi";
import Checkbox from "primevue/checkbox";
import { convertToEur, emptyBill, errorToast } from "@/utils";
import Button from "primevue/button";
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
import BaseCard from "@/components/UI/BaseCard.vue";
import BottomNavigation from "@/components/UI/BottomNavigation.vue";
import BaseItem from "@/components/UI/BaseItem.vue";
import ConfirmDialog from "primevue/confirmdialog";
import { useConfirm } from "primevue/useconfirm";
import { useToast } from "primevue/usetoast";
import Sidebar from "primevue/sidebar";
import BillModal from "@/components/Bills/BillModal.vue";
import { useRouter } from "vue-router";
export default defineComponent({
name: "CheckoutView",
// eslint-disable-next-line
components: { BaseItem, BaseCard, WaveSpinner, Checkbox, Button, BottomNavigation, ConfirmDialog, Sidebar, BillModal },
props: { id: { type: String, default: "0" } },
setup(props) {
const confirm = useConfirm();
const toast = useToast();
const router = useRouter();
const table = computed(() => parseInt(props.id));
const orders = ref<service_Order[]>([]);
const orderFilter = ref<number[]>([]);
const isLoading = ref(false);
const applyFilterLoading = ref(false);
const checkAll = ref(false);
const total = ref(0);
const bill = ref<service_Bill>({ ...emptyBill });
const billModal = ref(false);
function checkAllCheck() {
if (orderFilter.value) checkAll.value = orderFilter.value.length === orders.value.length;
}
function calculateTotal() {
let temp = 0;
orders.value.forEach((order) => {
if (order.id && orderFilter.value.includes(order.id)) temp += order.order_item.price;
});
total.value = temp;
}
watch(orderFilter, () => {
checkAllCheck();
calculateTotal();
});
getData();
function getData() {
isLoading.value = true;
OrdersService.getOrders(table.value, false)
.then((res) => {
orders.value = res;
setAllOrdersSelected();
checkAllCheck();
})
.finally(() => (isLoading.value = false));
}
function setAllOrdersSelected() {
const temp: number[] = [];
orders.value.forEach((order) => order.id && temp.push(order.id));
orderFilter.value = temp;
}
function checkAllClicked() {
if (orderFilter.value.length === orders.value.length) {
orderFilter.value = [];
} else {
setAllOrdersSelected();
}
}
function generateBill() {
applyFilterLoading.value = true;
confirm.require({
message: "Alle ausgewählte Bestellungen abrechnen?",
header: "Abrechnen",
icon: "pi pi-exclamation-triangle",
acceptClass: "p-button-danger",
rejectClass: "p-button-secondary",
accept: () => {
BillsService.postBills(table.value, orderFilter.value.toString())
.then((res) => {
bill.value = res;
billModal.value = true;
getData();
})
.catch((err) => errorToast(toast, err.body.error))
.finally(() => (applyFilterLoading.value = false));
},
reject: () => {
applyFilterLoading.value = false;
},
});
}
function billModalClosed() {
if (orderFilter.value.length === 0) {
router.push({ name: "Bills" });
}
}
return {
orders,
orderFilter,
checkAll,
checkAllClicked,
convertToEur,
generateBill,
isLoading,
applyFilterLoading,
total,
table,
bill,
billModal,
billModalClosed,
};
},
});
</script>
<style scoped>
.v-enter-active {
transition: opacity 0.2s ease-in;
}
.v-enter-from {
opacity: 0;
}
.field-checkbox:last-child {
margin-bottom: 0.25rem;
}
</style>

View file

@ -0,0 +1,21 @@
<template>
<BaseCard>
<BaseItem bgColor="c">
<div class="p-3 text-center">{{ message }}</div>
</BaseItem>
</BaseCard>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue";
import BaseItem from "@/components/UI/BaseItem.vue";
export default defineComponent({
name: "EmptyView",
components: { BaseItem, BaseCard },
props: { message: { type: String, default: "Bis jetzt noch nichts..." } },
});
</script>
<style></style>

View file

@ -0,0 +1,78 @@
<template>
<BaseCard>
<Transition>
<WaveSpinner v-if="isLoading" />
<OrderItemList
v-else
:orderItems="currentOrderItems"
:emptyOrderItem="emptyOrderItem"
@orderItemChanged="(item) => orderItemChanged(item)"
@orderItemDeleted="(item) => orderItemDeleted(item)"
@orderItemCreated="(item) => orderItemCreated(item)"
/>
</Transition>
</BaseCard>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref, watch } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue";
import { service_OrderItem, types_ItemType } from "@/services/openapi";
import OrderItemList from "@/components/OrderItem/OrderItemList.vue";
import { useStore } from "vuex";
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
export default defineComponent({
name: "ItemView",
components: { OrderItemList, BaseCard, WaveSpinner },
props: { id: { type: String, default: "0" } },
setup(props) {
const store = useStore();
const isLoading = ref(true);
const orderItems = computed(() => store.getters.getOrderItems);
const currentOrderItems = ref();
const emptyOrderItem = reactive<service_OrderItem>({ description: "", item_type: 0, price: 0 });
const intId = ref<types_ItemType>(parseInt(props.id));
getData();
async function getData() {
isLoading.value = true;
intId.value = parseInt(props.id);
await store.dispatch("getOrderItems", intId.value);
emptyOrderItem.item_type = intId.value;
refreshMap();
isLoading.value = false;
}
function refreshMap() {
currentOrderItems.value = orderItems.value.get(intId.value);
}
watch(props, () => getData());
function orderItemChanged(item: service_OrderItem) {
store.dispatch("updateOrderItem", item);
refreshMap();
}
function orderItemDeleted(item: service_OrderItem) {
store.dispatch("deleteOrderItem", item);
refreshMap();
}
function orderItemCreated(item: service_OrderItem) {
store.dispatch("addOrderItem", item);
refreshMap();
}
return { currentOrderItems, orderItemChanged, orderItemDeleted, orderItemCreated, emptyOrderItem, isLoading };
},
});
</script>
<style scoped>
.v-enter-active {
transition: opacity 0.2s ease-in;
}
.v-enter-from {
opacity: 0;
}
</style>

View file

@ -0,0 +1,155 @@
<template>
<BaseCard>
<Transition>
<WaveSpinner v-if="isLoading" />
<EmptyView v-else-if="empty" message="Keine offenen Bestellungen" />
<EmptyView v-else-if="!user.show_cold_drinks && !user.show_hot_drinks" message="Keine Bestellungen in den Einstellungen gewählt" />
<div v-else>
<template v-if="user.show_hot_drinks">
<OrderSection
v-for="[key, orders] in otherOrders"
v-bind:key="key"
:orders="orders"
:title="'Tisch ' + orders[0].table_id"
@filterOrders="(id) => filterOrder(id)"
/>
</template>
<template v-if="user.show_cold_drinks">
<OrderSection :orders="coldOrders" :itemType="types_ItemType.ColdDrink" @filterOrders="(id) => filterOrder(id)" />
</template>
</div>
</Transition>
</BaseCard>
</template>
<script lang="ts">
import { computed, defineComponent, onUnmounted, provide, ref } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue";
import { OrdersService, service_Order, types_ItemType, user_User } from "@/services/openapi";
import { detailedItemTypeIcon, detailedItemTypeString, NotifierType, WebSocketMsg } from "@/utils";
import { WEBSOCKET_ENDPOINT_URL } from "@/main";
import EmptyView from "@/views/Empty.vue";
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
import { disabled, loading } from "@/keys";
import OrderSection from "@/components/Orders/OrderSection.vue";
import { useStore } from "vuex";
export default defineComponent({
name: "OrderView",
components: { OrderSection, EmptyView, BaseCard, WaveSpinner },
setup() {
const store = useStore();
const isLoading = ref(true);
const isDisabled = ref(false);
provide(disabled, isDisabled);
provide(loading, isDisabled);
const orders = ref<service_Order[]>([]);
const user = computed<user_User>(() => store.getters.getUser);
const empty = computed(() => {
return (
(user.value.show_cold_drinks && user.value.show_hot_drinks && orders.value.length === 0) ||
(user.value.show_cold_drinks && !user.value.show_hot_drinks && coldOrders.value.length === 0) ||
(!user.value.show_cold_drinks && user.value.show_hot_drinks && otherOrders.value.size === 0)
);
});
const otherOrders = computed(() => {
const temp = new Map<number, service_Order[]>();
orders.value.forEach((order) => {
if (order.order_item.item_type === types_ItemType.ColdDrink) return;
const existing = temp.get(order.table_id);
if (existing) {
existing.push(order);
} else {
temp.set(order.table_id, [order]);
}
});
return new Map([...temp.entries()].sort());
});
const coldOrders = computed(() => orders.value.filter((order) => order.order_item.item_type === types_ItemType.ColdDrink));
const ws = ref<WebSocket | null>(null);
getData();
function getData() {
isLoading.value = true;
OrdersService.getOrders()
.then((res) => (orders.value = res))
.finally(() => {
isLoading.value = false;
startWebsocket();
});
}
onUnmounted(() => stopWebsocket());
function filterOrder(id: number) {
orders.value = orders.value.filter((old) => old.id !== id);
}
function startWebsocket() {
ws.value = new WebSocket(WEBSOCKET_ENDPOINT_URL);
ws.value.addEventListener("message", parseWebsocket);
ws.value.addEventListener("error", handleWebsocketError);
}
function stopWebsocket() {
if (ws.value) {
ws.value.removeEventListener("message", parseWebsocket);
ws.value.removeEventListener("error", handleWebsocketError);
ws.value.close();
}
}
function handleWebsocketError() {
stopWebsocket();
setTimeout(() => {
startWebsocket();
}, 1000);
}
function parseWebsocket(evt: Event) {
isDisabled.value = true;
const messageEvent = evt as MessageEvent;
const webSocketMsg: WebSocketMsg = JSON.parse(messageEvent.data);
if (webSocketMsg.type === NotifierType.Create) {
orders.value.push(webSocketMsg.payload[0]);
} else if (webSocketMsg.type === NotifierType.Delete) {
orders.value = orders.value.filter((o) => o.id !== webSocketMsg.payload[0].id);
} else if (webSocketMsg.type === NotifierType.DeleteAll) {
webSocketMsg.payload.forEach((obj) => {
orders.value = orders.value.filter((o) => o.id !== obj.id);
});
}
sortOrders();
isDisabled.value = false;
}
function sortOrders() {
orders.value.sort((a, b) => (a.updated_at && b.updated_at ? a.updated_at - b.updated_at : 0));
}
return {
orders,
otherOrders,
coldOrders,
filterOrder,
types_ItemType,
isLoading,
isDisabled,
detailedItemTypeString,
detailedItemTypeIcon,
user,
empty,
};
},
});
</script>
<style scoped>
.v-enter-active {
transition: opacity 0.2s ease-in;
}
.v-enter-from {
opacity: 0;
}
</style>

View file

@ -0,0 +1,50 @@
<template>
<BaseCard>
<Transition>
<WaveSpinner v-if="isLoading" />
<EmptyView v-else-if="tables && tables.length === 0" message="Keine Tische" />
<div v-else class="grid">
<TableCard v-for="table in tables" v-bind:key="table.id" :table="table" />
</div>
</Transition>
</BaseCard>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue";
import { useStore } from "vuex";
import TableCard from "@/components/Tables/TableCard.vue";
import EmptyView from "@/views/Empty.vue";
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
export default defineComponent({
name: "TablesView",
components: { WaveSpinner, EmptyView, TableCard, BaseCard },
setup() {
const isLoading = ref(false);
const store = useStore();
const tables = computed(() => store.getters.getTables);
getData();
function getData() {
isLoading.value = true;
store.dispatch("fetchTables").then((res) => {
store.commit("setTables", res);
isLoading.value = false;
});
}
return { tables, isLoading };
},
});
</script>
<style scoped>
.v-enter-active {
transition: opacity 0.2s ease-in;
}
.v-enter-from {
opacity: 0;
}
</style>

View file

@ -0,0 +1,12 @@
import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue", () => {
it("renders props.msg when passed", () => {
const msg = "new message";
const wrapper = shallowMount(HelloWorld, {
props: { msg },
});
expect(wrapper.text()).toMatch(msg);
});
});

23
frontend/tsconfig.json Normal file
View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["webpack-env", "jest"],
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
"exclude": ["node_modules"]
}

7
frontend/vue.config.js Normal file
View file

@ -0,0 +1,7 @@
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
allowedHosts: 'all'
}
});

9160
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load diff

70
go.mod Normal file
View file

@ -0,0 +1,70 @@
module cafe
go 1.20
require (
github.com/gin-contrib/cors v1.4.0
github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/assert/v2 v2.2.0
github.com/gorilla/websocket v1.5.0
github.com/mitchellh/mapstructure v1.5.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.16.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.1
github.com/unjx-de/go-folder v1.0.7
gorm.io/driver/mysql v1.5.1
gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.2
gorm.io/plugin/soft_delete v1.2.1
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.9.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

662
go.sum Normal file
View file

@ -0,0 +1,662 @@
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.44.3/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 v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
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=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
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/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
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/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.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/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/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/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/unjx-de/go-folder v1.0.7 h1:OVKvqjcVB0ASidVshYndRtkmlqS1h6MIhSr0vqX3Q6A=
github.com/unjx-de/go-folder v1.0.7/go.mod h1:sbcRrRgLE49QI6CZqGBMdneRuNOOhoRU1gx9DYlyD2g=
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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
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/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
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-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-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
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-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/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/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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-20200116001909-b77594299b42/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-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/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/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/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/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
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/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c=
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU=
gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk=
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/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

55
hub/hub.go Normal file
View file

@ -0,0 +1,55 @@
package hub
import (
"cafe/service"
"github.com/sirupsen/logrus"
)
type (
NotifierChan chan service.WebSocketMsg
Hub struct {
Notifier NotifierChan
NewClients chan NotifierChan
ClosingClients chan NotifierChan
clients map[NotifierChan]struct{}
}
)
func (h *Hub) Initialize() {
h.Notifier = make(NotifierChan)
h.NewClients = make(chan NotifierChan)
h.ClosingClients = make(chan NotifierChan)
h.clients = make(map[NotifierChan]struct{})
go h.listen()
go func() {
for {
if msg, ok := <-service.LiveCh; ok {
h.Notifier <- msg
}
}
}()
}
func (h *Hub) listen() {
for {
select {
case s := <-h.NewClients:
h.clients[s] = struct{}{}
logrus.WithField("openConnections", len(h.clients)).Trace("Websocket connection added")
case s := <-h.ClosingClients:
delete(h.clients, s)
logrus.WithField("openConnections", len(h.clients)).Trace("Websocket connection removed")
case event := <-h.Notifier:
for client := range h.clients {
select {
case client <- event:
default:
close(client)
delete(h.clients, client)
}
}
}
}
}

39
main.go Normal file
View file

@ -0,0 +1,39 @@
package main
import (
"cafe/api"
"cafe/config"
"cafe/service"
"cafe/user"
"fmt"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func main() {
gin.SetMode(gin.ReleaseMode)
config.Cafe.Database.Initialize(config.StorageDir)
config.Cafe.Database.MigrateHelper(service.Table{}, "table")
config.Cafe.Database.MigrateHelper(service.Order{}, "order")
config.Cafe.Database.MigrateHelper(service.OrderItem{}, "orderItem")
config.Cafe.Database.MigrateHelper(service.Bill{}, "bill")
config.Cafe.Database.MigrateHelper(service.BillItem{}, "billItem")
config.Cafe.Database.MigrateHelper(user.User{}, "user")
a := api.Api{}
service.Initialize()
a.Hub.Initialize()
a.Router = gin.New()
a.SetMiddlewares()
a.HandleStaticFiles()
a.SetupSwagger()
a.SetupRouter()
logrus.WithField("port", config.Cafe.Port).Info("Server running")
err := a.Router.Run(fmt.Sprintf(":%d", config.Cafe.Port))
if err != nil {
logrus.WithField("error", err).Fatal("Cannot start server")
}
}

25
scripts/dev.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/sh
parse_yaml() {
local prefix=$2
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @ | tr @ '\034')
sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 |
awk -F$fs '{
indent = length($1)/2;
vname[indent] = $2;
for (i in vname) {if (i > indent) {delete vname[i]}}
if (length($3) > 0) {
vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3);
}
}'
}
eval $(parse_yaml .gitlab/_common.gitlab-ci.yml)
echo "DOCKER_VERSION="$variables_DOCKER_VERSION >.env
echo "GOLANG_VERSION="$variables_GOLANG_VERSION >>.env
echo "NODE_VERSION="$variables_NODE_VERSION >>.env
echo "ALPINE_VERSION="$variables_ALPINE_VERSION >>.env
echo "DEBIAN_VERSION="$variables_DEBIAN_VERSION >>.env
echo "TELEPORT_VERSION="$variables_TELEPORT_VERSION >>.env

34
scripts/entrypoint.sh Executable file
View file

@ -0,0 +1,34 @@
#!/bin/sh
cat logo.txt
echo ""
if [ -n "$PUID" ] || [ -n "$PGID" ]; then
USER=appuser
HOME=/app
if ! grep -q "$USER" /etc/passwd; then
# Usage: addgroup [-g GID] [-S] [USER] GROUP
#
# Add a group or add a user to a group
# -g GID Group id
addgroup -g "$PGID" "$USER"
# Usage: adduser [OPTIONS] USER [GROUP]
# Create new user, or add USER to GROUP
# -h DIR Home directory
# -g GECOS GECOS field
# -G GRP Group
# -D Don't assign a password
# -H Don't create home directory
# -u UID User id
adduser -h "$HOME" -g "" -G "$USER" -D -H -u "$PUID" "$USER"
fi
chown "$USER":"$USER" "$HOME" -R
printf "UID: %s GID: %s\n\n" "$PUID" "$PGID"
exec su -c - $USER ./cafe
else
printf "WARNING: Running docker as root\n\n"
exec ./cafe
fi

15
scripts/swagger.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
action=$1
case $action in
"install")
go install github.com/swaggo/swag/cmd/swag@latest
;;
"init")
swag init -g api/swagger.go
;;
*)
exit 0
;;
esac

124
service/bill.go Normal file
View file

@ -0,0 +1,124 @@
package service
import (
"cafe/config"
"cafe/types"
"fmt"
"strconv"
"time"
)
type (
Bill struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
TableID uint64 `json:"table_id" validate:"required"`
Total float32 `json:"total" validate:"required"`
CreatedAt int64 `json:"created_at" validate:"optional"`
}
BillItem struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
BillID uint64 `json:"bill_id" validate:"required"`
Description string `json:"description" validate:"required"`
Total float32 `json:"total" validate:"required"`
Price float32 `json:"price" validate:"required"`
Amount uint64 `json:"amount" validate:"required"`
ItemType types.ItemType `json:"item_type" validate:"required"`
}
)
func DoesBillExist(id string) (Bill, error) {
var bill Bill
result := config.Cafe.Database.ORM.Limit(1).Find(&bill, id)
if result.RowsAffected == 0 {
return bill, fmt.Errorf(types.CannotFind.String())
}
return bill, nil
}
func GetAllBillItems(billId uint64) ([]BillItem, error) {
var billItems []BillItem
result := config.Cafe.Database.ORM.Where("bill_id = ?", billId).Find(&billItems)
if result.RowsAffected == 0 {
return billItems, fmt.Errorf(types.CannotFind.String())
}
return billItems, nil
}
func getDate(year string, month string, day string) (time.Time, error) {
yearI, yearErr := strconv.Atoi(year)
if yearErr != nil {
return time.Time{}, fmt.Errorf("jahr " + types.CannotParse.String())
}
monthI, monthErr := strconv.Atoi(month)
if monthErr != nil {
return time.Time{}, fmt.Errorf("monat " + types.CannotParse.String())
}
dayI, dayErr := strconv.Atoi(day)
if dayErr != nil {
return time.Time{}, fmt.Errorf("tag " + types.CannotParse.String())
}
loc, locErr := time.LoadLocation("Local")
if locErr != nil {
return time.Time{}, fmt.Errorf("timezone " + types.CannotParse.String())
}
return time.Date(yearI, time.Month(monthI), dayI, 0, 0, 0, 0, loc), nil
}
func GetAllBills(year string, month string, day string) ([]Bill, error) {
var bills []Bill
today, err := getDate(year, month, day)
if err != nil {
return bills, err
}
beginningOfDay := today.Unix()
endOfDay := today.Add(23 * time.Hour).Add(59 * time.Minute).Add(59 * time.Second).Unix()
config.Cafe.Database.ORM.Where("created_at BETWEEN ? AND ?", beginningOfDay, endOfDay).Order("created_at").Find(&bills)
return bills, nil
}
func CreateBill(options GetOrderOptions) (Bill, error) {
orders := GetAllOrdersForTable(options)
var bill Bill
var total float32 = 0
for _, order := range orders {
total += order.Total
}
bill.TableID = options.TableId
bill.Total = total
err := config.Cafe.Database.ORM.Create(&bill).Error
if err != nil {
return bill, fmt.Errorf(types.CannotCreate.String())
}
for _, order := range orders {
billItem := BillItem{
BillID: bill.ID,
Description: order.OrderItem.Description,
Total: order.Total,
Price: order.OrderItem.Price,
Amount: order.OrderCount,
ItemType: order.OrderItem.ItemType,
}
config.Cafe.Database.ORM.Create(&billItem)
}
ordersToDelete := GetAllOrdersForTable(GetOrderOptions{TableId: options.TableId, Grouped: false, Filter: options.Filter})
err = config.Cafe.Database.ORM.Delete(&ordersToDelete).Error
if err != nil {
return bill, fmt.Errorf(types.CannotDelete.String())
}
LiveCh <- WebSocketMsg{
Type: types.DeleteAll,
Payload: ordersToDelete,
}
return bill, nil
}
func DeleteBill(bill *Bill) error {
err := config.Cafe.Database.ORM.Delete(bill).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
billItemsToDelete, _ := GetAllBillItems(bill.ID)
config.Cafe.Database.ORM.Delete(&billItemsToDelete)
return nil
}

169
service/order.go Normal file
View file

@ -0,0 +1,169 @@
package service
import (
"cafe/config"
"cafe/types"
"fmt"
"time"
"gorm.io/gorm"
)
type (
Order struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
TableID uint64 `json:"table_id" validate:"required"`
OrderItemID uint64 `json:"order_item_id" validate:"required"`
OrderItem OrderItem `json:"order_item" validate:"required"`
UpdatedAt int64 `json:"updated_at" validate:"optional"`
IsServed bool `json:"is_served" default:"false" validate:"required"`
Total float32 `json:"total" validate:"required"`
OrderCount uint64 `json:"order_count" validate:"required"`
}
OrderItem struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
ItemType types.ItemType `json:"item_type" validate:"required"`
Description string `json:"description" validate:"required"`
Price float32 `json:"price" validate:"required"`
}
GetOrderOptions struct {
TableId uint64 `json:"table_id"`
Grouped bool `json:"grouped"`
Filter []string `json:"filter"`
}
)
func updateTableUpdatedAt(tx *gorm.DB, o *Order) {
var table Table
tx.Where("id = ?", o.TableID).First(&table)
table.UpdatedAt = time.Now().Unix()
tx.Save(&table)
}
func (o *Order) AfterCreate(tx *gorm.DB) (err error) {
updateTableUpdatedAt(tx, o)
return
}
func (o *Order) AfterDelete(tx *gorm.DB) (err error) {
updateTableUpdatedAt(tx, o)
return
}
func DoesOrderItemExist(id string) (OrderItem, error) {
var orderItem OrderItem
result := config.Cafe.Database.ORM.Limit(1).Find(&orderItem, id)
if result.RowsAffected == 0 {
return orderItem, fmt.Errorf(types.CannotFind.String())
}
return orderItem, nil
}
func DoesOrderExist(id string) (Order, error) {
var order Order
result := config.Cafe.Database.ORM.Limit(1).Find(&order, id)
if result.RowsAffected == 0 {
return order, fmt.Errorf(types.CannotFind.String())
}
return order, nil
}
func GetAllActiveOrders() []Order {
var orders []Order
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").Where("is_served = ?", 0).Order("updated_at").Find(&orders)
return orders
}
func GetAllOrdersForTable(options GetOrderOptions) []Order {
var orders []Order
if options.Grouped {
if len(options.Filter) == 0 {
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").Select("table_id, order_item_id, sum(price) as total, count(order_item_id) as order_count").Group("order_item_id").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
} else {
config.Cafe.Database.ORM.Model(&Order{}).Find(&orders, options.Filter).Joins("OrderItem").Select("table_id, order_item_id, sum(price) as total, count(order_item_id) as order_count").Group("order_item_id").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
}
} else {
if len(options.Filter) == 0 {
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
} else {
config.Cafe.Database.ORM.Model(&Order{}).Find(&orders, options.Filter).Where("table_id = ?", options.TableId).Find(&orders)
}
}
return orders
}
func CreateOrder(order *Order) error {
err := config.Cafe.Database.ORM.Create(order).Error
if err != nil {
return fmt.Errorf(types.CannotCreate.String())
}
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").First(order)
LiveCh <- WebSocketMsg{
Type: types.Create,
Payload: []Order{*order},
}
return nil
}
func UpdateOrder(old *Order, new *Order) error {
err := config.Cafe.Database.ORM.First(old).Updates(new).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
if new.IsServed {
LiveCh <- WebSocketMsg{
Type: types.Delete,
Payload: []Order{*new},
}
}
return nil
}
func DeleteOrder(tableId string, orderItemId string) error {
var order Order
err := config.Cafe.Database.ORM.Where("table_id = ? AND order_item_id = ?", tableId, orderItemId).Last(&order).Error
if err != nil {
return fmt.Errorf(types.CannotFind.String())
}
err = config.Cafe.Database.ORM.Delete(&order).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
LiveCh <- WebSocketMsg{
Type: types.Delete,
Payload: []Order{order},
}
return nil
}
func GetOrderItemsForType(itemType string) []OrderItem {
var orderItems []OrderItem
config.Cafe.Database.ORM.Order("description").Where("item_type = ?", types.ParseItemType(itemType)).Find(&orderItems)
return orderItems
}
func CreateOrderItem(oderItem *OrderItem) error {
err := config.Cafe.Database.ORM.Create(oderItem).Error
if err != nil {
return fmt.Errorf(types.CannotCreate.String())
}
return nil
}
func UpdateOrderItem(old *OrderItem, new *OrderItem) error {
err := config.Cafe.Database.ORM.First(old).Updates(new).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
return nil
}
func DeleteOrderItem(oderItem *OrderItem) error {
err := config.Cafe.Database.ORM.Delete(oderItem).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
return nil
}

18
service/service.go Normal file
View file

@ -0,0 +1,18 @@
package service
import (
"cafe/types"
)
type (
WebSocketMsg struct {
Type types.NotifierType `json:"type"`
Payload []Order `json:"payload"`
}
)
var LiveCh chan WebSocketMsg
func Initialize() {
LiveCh = make(chan WebSocketMsg)
}

75
service/table.go Normal file
View file

@ -0,0 +1,75 @@
package service
import (
"cafe/config"
"cafe/types"
"fmt"
"gorm.io/plugin/soft_delete"
)
type Table struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
OrderCount uint64 `json:"order_count" validate:"required"`
Total float32 `json:"total" validate:"required"`
UpdatedAt int64 `json:"updated_at" validate:"optional"`
IsDeleted soft_delete.DeletedAt `gorm:"softDelete:flag" json:"is_deleted" swaggerignore:"true"`
}
func GetAllTables() []Table {
var tables []Table
config.Cafe.Database.ORM.Model(
&Table{},
).Joins(
"left join orders on tables.id = orders.table_id",
).Joins(
"left join order_items on orders.order_item_id = order_items.id",
).Select(
"tables.id, tables.updated_at, sum(order_items.price) as total, count(orders.id) as order_count",
).Group(
"tables.id",
).Order("tables.id").Find(&tables)
return tables
}
func CreateNewTable() (Table, error) {
var table Table
var err error
result := config.Cafe.Database.ORM.Unscoped().Where("is_deleted = ?", 1).Limit(1).Find(&table)
if result.RowsAffected == 0 {
err = config.Cafe.Database.ORM.Create(&table).Error
} else {
table.IsDeleted = 0
err = config.Cafe.Database.ORM.Unscoped().Save(&table).Error
}
if err != nil {
return table, fmt.Errorf(types.CannotCreate.String())
}
return table, nil
}
func DeleteLatestTable() error {
var table Table
err := config.Cafe.Database.ORM.Model(
&Table{},
).Joins(
"left join orders on tables.id = orders.table_id",
).Joins(
"left join order_items on orders.order_item_id = order_items.id",
).Select(
"tables.id, count(orders.id) as order_count",
).Group(
"tables.id",
).Last(&table).Error
if err != nil {
return fmt.Errorf(types.CannotFind.String())
}
if table.OrderCount != 0 {
return fmt.Errorf(types.StillInUse.String())
}
err = config.Cafe.Database.ORM.Delete(&table).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
return nil
}

12
templates/index.html Normal file
View file

@ -0,0 +1,12 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Test</title>
</head>
<body>
<div>Working</div>
</body>
</html>

59
types/types.go Normal file
View file

@ -0,0 +1,59 @@
package types
type (
ErrorResponses uint
ItemType uint
NotifierType uint
)
const (
Create NotifierType = iota
Delete
DeleteAll
)
const (
Food ItemType = iota
ColdDrink
HotDrink
)
const (
MissingInformation ErrorResponses = iota
CannotCreate
CannotUpdate
CannotDelete
CannotFind
StillInUse
CannotParse
)
func ParseItemType(itemType string) ItemType {
switch itemType {
case "0":
return Food
case "1":
return ColdDrink
default:
return HotDrink
}
}
func (e ErrorResponses) String() string {
switch e {
case MissingInformation:
return "fehlende Informationen"
case CannotCreate:
return "kann nicht gespeichert werden"
case CannotUpdate:
return "kann nicht geändert werden"
case CannotDelete:
return "kann nicht gelöscht werden"
case CannotFind:
return "kann nicht gefunden werden"
case StillInUse:
return "noch in Verwendung"
default:
return "kann nicht verarbeitet werden"
}
}

23
types/types_test.go Normal file
View file

@ -0,0 +1,23 @@
package types
import (
"testing"
"github.com/go-playground/assert/v2"
)
func TestErrorResponses_String(t *testing.T) {
assert.Equal(t, MissingInformation.String(), "fehlende Informationen")
assert.Equal(t, CannotCreate.String(), "kann nicht gespeichert werden")
assert.Equal(t, CannotUpdate.String(), "kann nicht geändert werden")
assert.Equal(t, CannotDelete.String(), "kann nicht gelöscht werden")
assert.Equal(t, CannotFind.String(), "kann nicht gefunden werden")
assert.Equal(t, StillInUse.String(), "noch in Verwendung")
assert.Equal(t, CannotParse.String(), "kann nicht verarbeitet werden")
}
func TestParseItemType(t *testing.T) {
assert.Equal(t, ParseItemType("0"), Food)
assert.Equal(t, ParseItemType("1"), ColdDrink)
assert.Equal(t, ParseItemType("2"), HotDrink)
}

42
user/user.go Normal file
View file

@ -0,0 +1,42 @@
package user
import (
"cafe/config"
"cafe/types"
"fmt"
)
type User struct {
Username string `gorm:"primaryKey" json:"username" validate:"required"`
ShowColdDrinks bool `json:"show_cold_drinks" validate:"required"`
ShowHotDrinks bool `json:"show_hot_drinks" validate:"required"`
}
func DoesUserExist(username string) (User, error) {
var user User
result := config.Cafe.Database.ORM.Limit(1).Find(&user, "username = ?", username)
if result.RowsAffected == 0 {
return user, fmt.Errorf(types.CannotFind.String())
}
return user, nil
}
func GetUserOrCreate(username string) (User, error) {
var user User
err := config.Cafe.Database.ORM.Where(User{Username: username}).Attrs(User{ShowHotDrinks: true, ShowColdDrinks: true}).FirstOrCreate(&user).Error
if err != nil {
return user, fmt.Errorf(types.CannotCreate.String())
}
return user, nil
}
func UpdateUser(old *User, new *User) error {
err := config.Cafe.Database.ORM.First(old).Updates(map[string]interface{}{
"Username": new.Username,
"ShowColdDrinks": new.ShowColdDrinks,
"ShowHotDrinks": new.ShowHotDrinks}).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
return nil
}

36
websocket/websocket.go Normal file
View file

@ -0,0 +1,36 @@
package websocket
import (
"cafe/config"
"net/http"
"github.com/gorilla/websocket"
)
func inAllowedHosts(str string) bool {
for _, a := range config.Cafe.AllowedHosts {
if a == str {
return true
}
}
return false
}
var Upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return inAllowedHosts(origin)
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func ReadPump(conn *websocket.Conn) {
defer conn.Close()
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
}