Restructure project

This commit is contained in:
Florian Hoss 2023-07-04 23:07:23 +02:00
parent e44e7caa11
commit 16b2f17301
46 changed files with 1744 additions and 1265 deletions

4
.air.toml Normal file
View file

@ -0,0 +1,4 @@
[build]
bin = "tmp/cafe-plaetschwiesle"
cmd = "go build -o tmp/cafe-plaetschwiesle cmd/cafe-plaetschwiesle/cafe-plaetschwiesle.go"
exclude_dir = [".gitlab", "docker", "scripts", "frontend", "storage", "tmp", "docs"]

View file

@ -1,52 +0,0 @@
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")
}

View file

@ -1,62 +0,0 @@
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")
}

View file

@ -1,112 +0,0 @@
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)
}

View file

@ -1,258 +0,0 @@
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)
}
}

View file

@ -1,54 +0,0 @@
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)
}
}

View file

@ -1,60 +0,0 @@
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)
}
}

View file

@ -1,26 +0,0 @@
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
})
}

View file

@ -1,31 +0,0 @@
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")
}
}

View file

@ -1,16 +0,0 @@
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"`
}

View file

@ -0,0 +1,30 @@
package main
import (
"fmt"
"log"
"net/http"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/controller"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/env"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/logging"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/router"
"go.uber.org/zap"
)
func main() {
env, err := env.Parse()
if err != nil {
log.Fatal(err)
}
zap.ReplaceGlobals(logging.CreateLogger(env.LogLevel))
r := router.InitRouter()
c := controller.NewController(env)
router.SetupRoutes(r, c, env)
zap.L().Info("starting server", zap.String("url", fmt.Sprintf("http://localhost:%d", env.Port)), zap.String("version", env.Version))
if err := r.Start(fmt.Sprintf(":%d", env.Port)); err != http.ErrServerClosed {
zap.L().Fatal("cannot start server", zap.Error(err))
}
}

View file

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

View file

@ -1,72 +0,0 @@
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()
}

View file

@ -1,80 +0,0 @@
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")
}

View file

@ -111,7 +111,7 @@ services:
args: args:
- GOLANG_VERSION=${GOLANG_VERSION} - GOLANG_VERSION=${GOLANG_VERSION}
container_name: cafe-backend container_name: cafe-backend
entrypoint: air --build.exclude_dir "node_modules,frontend,static,docs,storage,tmp,dist" command: air -c .air.toml
environment: environment:
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
@ -122,7 +122,7 @@ services:
- 'traefik.http.routers.backend.tls=true' - 'traefik.http.routers.backend.tls=true'
- 'traefik.http.routers.backend.middlewares=authelia@docker' - 'traefik.http.routers.backend.middlewares=authelia@docker'
expose: expose:
- 5000 - 8080
networks: networks:
- net - net
volumes: volumes:

View file

@ -8,8 +8,7 @@ COPY ./go.sum .
RUN go mod download RUN go mod download
RUN go install github.com/cosmtrek/air@latest RUN go install github.com/cosmtrek/air@latest
COPY ./.air.toml .
ENV VERSION=v0.0.0-DEV ENV VERSION=v0.0.0-DEV
ENV BUILD_TIME=2023-06-01T08:07:43.454Z ENV BUILD_TIME=2023-06-01T08:07:43.454Z
CMD ["air"]

View file

@ -18,7 +18,6 @@ import "primeicons/primeicons.css";
import "primeflex/primeflex.css"; import "primeflex/primeflex.css";
export const API_ENDPOINT_URL = window.origin + "/api"; 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; OpenAPI.BASE = API_ENDPOINT_URL;
async function getHealth() { async function getHealth() {

View file

@ -27,7 +27,7 @@ import { computed, defineComponent, onUnmounted, provide, ref } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue"; import BaseCard from "@/components/UI/BaseCard.vue";
import { OrdersService, service_Order, types_ItemType, user_User } from "@/services/openapi"; import { OrdersService, service_Order, types_ItemType, user_User } from "@/services/openapi";
import { detailedItemTypeIcon, detailedItemTypeString, NotifierType, WebSocketMsg } from "@/utils"; import { detailedItemTypeIcon, detailedItemTypeString, NotifierType, WebSocketMsg } from "@/utils";
import { WEBSOCKET_ENDPOINT_URL } from "@/main"; import { API_ENDPOINT_URL } from "@/main";
import EmptyView from "@/views/Empty.vue"; import EmptyView from "@/views/Empty.vue";
import WaveSpinner from "@/components/UI/WaveSpinner.vue"; import WaveSpinner from "@/components/UI/WaveSpinner.vue";
import { disabled, loading } from "@/keys"; import { disabled, loading } from "@/keys";
@ -68,7 +68,7 @@ export default defineComponent({
return new Map([...temp.entries()].sort()); return new Map([...temp.entries()].sort());
}); });
const coldOrders = computed(() => orders.value.filter((order) => order.order_item.item_type === types_ItemType.ColdDrink)); const coldOrders = computed(() => orders.value.filter((order) => order.order_item.item_type === types_ItemType.ColdDrink));
const ws = ref<WebSocket | null>(null); const sse = ref<EventSource | null>(null);
getData(); getData();
function getData() { function getData() {
@ -77,51 +77,36 @@ export default defineComponent({
.then((res) => (orders.value = res)) .then((res) => (orders.value = res))
.finally(() => { .finally(() => {
isLoading.value = false; isLoading.value = false;
startWebsocket(); setupSSE();
}); });
} }
onUnmounted(() => stopWebsocket()); onUnmounted(() => sse.value && sse.value.close());
addEventListener("beforeunload", () => {
sse.value && sse.value.close();
});
function filterOrder(id: number) { function filterOrder(id: number) {
orders.value = orders.value.filter((old) => old.id !== id); orders.value = orders.value.filter((old) => old.id !== id);
} }
function startWebsocket() { function setupSSE() {
ws.value = new WebSocket(WEBSOCKET_ENDPOINT_URL); sse.value = new EventSource(API_ENDPOINT_URL + "/orders/sse?stream=sse");
ws.value.addEventListener("message", parseWebsocket); sse.value.onmessage = (evt: Event) => {
ws.value.addEventListener("error", handleWebsocketError); isDisabled.value = true;
} const messageEvent = evt as MessageEvent;
const webSocketMsg: WebSocketMsg = JSON.parse(messageEvent.data);
function stopWebsocket() { if (webSocketMsg.type === NotifierType.Create) {
if (ws.value) { orders.value.push(webSocketMsg.payload[0]);
ws.value.removeEventListener("message", parseWebsocket); } else if (webSocketMsg.type === NotifierType.Delete) {
ws.value.removeEventListener("error", handleWebsocketError); orders.value = orders.value.filter((o) => o.id !== webSocketMsg.payload[0].id);
ws.value.close(); } else if (webSocketMsg.type === NotifierType.DeleteAll) {
} webSocketMsg.payload.forEach((obj) => {
} orders.value = orders.value.filter((o) => o.id !== obj.id);
});
function handleWebsocketError() { }
stopWebsocket(); sortOrders();
setTimeout(() => { isDisabled.value = false;
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() { function sortOrders() {

61
go.mod
View file

@ -1,70 +1,61 @@
module cafe module gitlab.unjx.de/flohoss/cafe-plaetschwiesle
go 1.20 go 1.20
require ( require (
github.com/gin-contrib/cors v1.4.0 github.com/caarlos0/env/v8 v8.0.0
github.com/gin-contrib/static v0.0.1 github.com/containrrr/shoutrrr v0.7.1
github.com/gin-gonic/gin v1.9.1 github.com/go-playground/validator/v10 v10.14.1
github.com/go-playground/assert/v2 v2.2.0 github.com/labstack/echo/v4 v4.10.2
github.com/gorilla/websocket v1.5.0 github.com/r3labs/sse/v2 v2.10.0
github.com/mitchellh/mapstructure v1.5.0 github.com/stretchr/testify v1.8.4
github.com/sirupsen/logrus v1.9.3 github.com/swaggo/echo-swagger v1.4.0
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/swaggo/swag v1.16.1
github.com/unjx-de/go-folder v1.0.7 go.uber.org/zap v1.24.0
gorm.io/driver/mysql v1.5.1 gorm.io/driver/mysql v1.5.1
gorm.io/driver/sqlite v1.5.2 gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.2 gorm.io/gorm v1.25.2
gorm.io/plugin/soft_delete v1.2.1 gorm.io/plugin/soft_delete v1.2.1
moul.io/zapgorm2 v1.3.0
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.9.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/fatih/color v1.15.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // 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/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.4 // indirect github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.4 // 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/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/spf13/afero v1.9.5 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/spf13/cast v1.5.1 // indirect go.uber.org/atomic v1.11.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect go.uber.org/multierr v1.11.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/crypto v0.10.0 // indirect
golang.org/x/net v0.11.0 // indirect golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect golang.org/x/text v0.10.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect golang.org/x/tools v0.10.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

780
go.sum

File diff suppressed because it is too large Load diff

View file

@ -1,55 +0,0 @@
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)
}
}
}
}
}

View file

@ -1,11 +1,11 @@
package service package controller
import ( import (
"cafe/config"
"cafe/types"
"fmt" "fmt"
"strconv" "strconv"
"time" "time"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
) )
type ( type (
@ -27,18 +27,18 @@ type (
} }
) )
func DoesBillExist(id string) (Bill, error) { func (c *Controller) DoesBillExist(id string) (Bill, error) {
var bill Bill var bill Bill
result := config.Cafe.Database.ORM.Limit(1).Find(&bill, id) result := c.orm.Limit(1).Find(&bill, id)
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
return bill, fmt.Errorf(types.CannotFind.String()) return bill, fmt.Errorf(types.CannotFind.String())
} }
return bill, nil return bill, nil
} }
func GetAllBillItems(billId uint64) ([]BillItem, error) { func (c *Controller) GetAllBillItems(billId uint64) ([]BillItem, error) {
var billItems []BillItem var billItems []BillItem
result := config.Cafe.Database.ORM.Where("bill_id = ?", billId).Find(&billItems) result := c.orm.Where("bill_id = ?", billId).Find(&billItems)
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
return billItems, fmt.Errorf(types.CannotFind.String()) return billItems, fmt.Errorf(types.CannotFind.String())
} }
@ -65,7 +65,7 @@ func getDate(year string, month string, day string) (time.Time, error) {
return time.Date(yearI, time.Month(monthI), dayI, 0, 0, 0, 0, loc), nil return time.Date(yearI, time.Month(monthI), dayI, 0, 0, 0, 0, loc), nil
} }
func GetAllBills(year string, month string, day string) ([]Bill, error) { func (c *Controller) GetAllBills(year string, month string, day string) ([]Bill, error) {
var bills []Bill var bills []Bill
today, err := getDate(year, month, day) today, err := getDate(year, month, day)
if err != nil { if err != nil {
@ -73,12 +73,12 @@ func GetAllBills(year string, month string, day string) ([]Bill, error) {
} }
beginningOfDay := today.Unix() beginningOfDay := today.Unix()
endOfDay := today.Add(23 * time.Hour).Add(59 * time.Minute).Add(59 * time.Second).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) c.orm.Where("created_at BETWEEN ? AND ?", beginningOfDay, endOfDay).Order("created_at").Find(&bills)
return bills, nil return bills, nil
} }
func CreateBill(options GetOrderOptions) (Bill, error) { func (c *Controller) createBill(options GetOrderOptions) (Bill, error) {
orders := GetAllOrdersForTable(options) orders := c.getAllOrdersForTable(options)
var bill Bill var bill Bill
var total float32 = 0 var total float32 = 0
for _, order := range orders { for _, order := range orders {
@ -86,7 +86,7 @@ func CreateBill(options GetOrderOptions) (Bill, error) {
} }
bill.TableID = options.TableId bill.TableID = options.TableId
bill.Total = total bill.Total = total
err := config.Cafe.Database.ORM.Create(&bill).Error err := c.orm.Create(&bill).Error
if err != nil { if err != nil {
return bill, fmt.Errorf(types.CannotCreate.String()) return bill, fmt.Errorf(types.CannotCreate.String())
} }
@ -99,26 +99,26 @@ func CreateBill(options GetOrderOptions) (Bill, error) {
Amount: order.OrderCount, Amount: order.OrderCount,
ItemType: order.OrderItem.ItemType, ItemType: order.OrderItem.ItemType,
} }
config.Cafe.Database.ORM.Create(&billItem) c.orm.Create(&billItem)
} }
ordersToDelete := GetAllOrdersForTable(GetOrderOptions{TableId: options.TableId, Grouped: false, Filter: options.Filter}) ordersToDelete := c.getAllOrdersForTable(GetOrderOptions{TableId: options.TableId, Grouped: false, Filter: options.Filter})
err = config.Cafe.Database.ORM.Delete(&ordersToDelete).Error err = c.orm.Delete(&ordersToDelete).Error
if err != nil { if err != nil {
return bill, fmt.Errorf(types.CannotDelete.String()) return bill, fmt.Errorf(types.CannotDelete.String())
} }
LiveCh <- WebSocketMsg{ c.publishMessage(StatusMessage{
Type: types.DeleteAll, Type: types.DeleteAll,
Payload: ordersToDelete, Payload: ordersToDelete,
} })
return bill, nil return bill, nil
} }
func DeleteBill(bill *Bill) error { func (c *Controller) deleteBill(bill *Bill) error {
err := config.Cafe.Database.ORM.Delete(bill).Error err := c.orm.Delete(bill).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotDelete.String()) return fmt.Errorf(types.CannotDelete.String())
} }
billItemsToDelete, _ := GetAllBillItems(bill.ID) billItemsToDelete, _ := c.GetAllBillItems(bill.ID)
config.Cafe.Database.ORM.Delete(&billItemsToDelete) c.orm.Delete(&billItemsToDelete)
return nil return nil
} }

View file

@ -0,0 +1,34 @@
package controller
import (
"github.com/r3labs/sse/v2"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/database"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/env"
"gorm.io/gorm"
)
type Controller struct {
orm *gorm.DB
env *env.Config
SSE *sse.Server
}
func NewController(env *env.Config) *Controller {
db := database.NewDatabaseConnection(&database.Database{
Host: env.DB_Host,
User: env.DB_User,
Password: env.DB_Password,
Database: env.DB_Database,
})
db.AutoMigrate(&Table{})
db.AutoMigrate(&Order{})
db.AutoMigrate(&OrderItem{})
db.AutoMigrate(&Bill{})
db.AutoMigrate(&BillItem{})
db.AutoMigrate(&User{})
ctrl := Controller{orm: db, env: env, SSE: sse.New()}
ctrl.setupEventChannel()
return &ctrl
}

View file

@ -0,0 +1,25 @@
package controller
import (
"encoding/json"
"github.com/r3labs/sse/v2"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
)
const ServerSideEvent = "sse"
type StatusMessage struct {
Type types.NotifierType `json:"type"`
Payload []Order `json:"payload"`
}
func (c *Controller) setupEventChannel() {
c.SSE.AutoReplay = false
c.SSE.CreateStream(ServerSideEvent)
}
func (c *Controller) publishMessage(msg StatusMessage) {
json, _ := json.Marshal(msg)
c.SSE.Publish(ServerSideEvent, &sse.Event{Data: json})
}

View file

@ -1,11 +1,10 @@
package service package controller
import ( import (
"cafe/config"
"cafe/types"
"fmt" "fmt"
"time" "time"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -52,116 +51,116 @@ func (o *Order) AfterDelete(tx *gorm.DB) (err error) {
return return
} }
func DoesOrderItemExist(id string) (OrderItem, error) { func (c *Controller) doesOrderItemExist(id string) (OrderItem, error) {
var orderItem OrderItem var orderItem OrderItem
result := config.Cafe.Database.ORM.Limit(1).Find(&orderItem, id) result := c.orm.Limit(1).Find(&orderItem, id)
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
return orderItem, fmt.Errorf(types.CannotFind.String()) return orderItem, fmt.Errorf(types.CannotFind.String())
} }
return orderItem, nil return orderItem, nil
} }
func DoesOrderExist(id string) (Order, error) { func (c *Controller) doesOrderExist(id string) (Order, error) {
var order Order var order Order
result := config.Cafe.Database.ORM.Limit(1).Find(&order, id) result := c.orm.Limit(1).Find(&order, id)
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
return order, fmt.Errorf(types.CannotFind.String()) return order, fmt.Errorf(types.CannotFind.String())
} }
return order, nil return order, nil
} }
func GetAllActiveOrders() []Order { func (c *Controller) getAllActiveOrders() []Order {
var orders []Order var orders []Order
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").Where("is_served = ?", 0).Order("updated_at").Find(&orders) c.orm.Model(&Order{}).Joins("OrderItem").Where("is_served = ?", 0).Order("updated_at").Find(&orders)
return orders return orders
} }
func GetAllOrdersForTable(options GetOrderOptions) []Order { func (c *Controller) getAllOrdersForTable(options GetOrderOptions) []Order {
var orders []Order var orders []Order
if options.Grouped { if options.Grouped {
if len(options.Filter) == 0 { 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) c.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 { } 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) c.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 { } else {
if len(options.Filter) == 0 { if len(options.Filter) == 0 {
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders) c.orm.Model(&Order{}).Joins("OrderItem").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
} else { } else {
config.Cafe.Database.ORM.Model(&Order{}).Find(&orders, options.Filter).Where("table_id = ?", options.TableId).Find(&orders) c.orm.Model(&Order{}).Find(&orders, options.Filter).Where("table_id = ?", options.TableId).Find(&orders)
} }
} }
return orders return orders
} }
func CreateOrder(order *Order) error { func (c *Controller) createOrder(order *Order) error {
err := config.Cafe.Database.ORM.Create(order).Error err := c.orm.Create(order).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotCreate.String()) return fmt.Errorf(types.CannotCreate.String())
} }
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").First(order) c.orm.Model(&Order{}).Joins("OrderItem").First(order)
LiveCh <- WebSocketMsg{ c.publishMessage(StatusMessage{
Type: types.Create, Type: types.Create,
Payload: []Order{*order}, Payload: []Order{*order},
} })
return nil return nil
} }
func UpdateOrder(old *Order, new *Order) error { func (c *Controller) updateOrder(old *Order, new *Order) error {
err := config.Cafe.Database.ORM.First(old).Updates(new).Error err := c.orm.First(old).Updates(new).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotUpdate.String()) return fmt.Errorf(types.CannotUpdate.String())
} }
if new.IsServed { if new.IsServed {
LiveCh <- WebSocketMsg{ c.publishMessage(StatusMessage{
Type: types.Delete, Type: types.Delete,
Payload: []Order{*new}, Payload: []Order{*new},
} })
} }
return nil return nil
} }
func DeleteOrder(tableId string, orderItemId string) error { func (c *Controller) deleteOrder(tableId string, orderItemId string) error {
var order Order var order Order
err := config.Cafe.Database.ORM.Where("table_id = ? AND order_item_id = ?", tableId, orderItemId).Last(&order).Error err := c.orm.Where("table_id = ? AND order_item_id = ?", tableId, orderItemId).Last(&order).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotFind.String()) return fmt.Errorf(types.CannotFind.String())
} }
err = config.Cafe.Database.ORM.Delete(&order).Error err = c.orm.Delete(&order).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotDelete.String()) return fmt.Errorf(types.CannotDelete.String())
} }
LiveCh <- WebSocketMsg{ c.publishMessage(StatusMessage{
Type: types.Delete, Type: types.Delete,
Payload: []Order{order}, Payload: []Order{order},
} })
return nil return nil
} }
func GetOrderItemsForType(itemType string) []OrderItem { func (c *Controller) getOrderItemsForType(itemType string) []OrderItem {
var orderItems []OrderItem var orderItems []OrderItem
config.Cafe.Database.ORM.Order("description").Where("item_type = ?", types.ParseItemType(itemType)).Find(&orderItems) c.orm.Order("description").Where("item_type = ?", types.ParseItemType(itemType)).Find(&orderItems)
return orderItems return orderItems
} }
func CreateOrderItem(oderItem *OrderItem) error { func (c *Controller) createOrderItem(oderItem *OrderItem) error {
err := config.Cafe.Database.ORM.Create(oderItem).Error err := c.orm.Create(oderItem).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotCreate.String()) return fmt.Errorf(types.CannotCreate.String())
} }
return nil return nil
} }
func UpdateOrderItem(old *OrderItem, new *OrderItem) error { func (c *Controller) updateOrderItem(old *OrderItem, new *OrderItem) error {
err := config.Cafe.Database.ORM.First(old).Updates(new).Error err := c.orm.First(old).Updates(new).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotUpdate.String()) return fmt.Errorf(types.CannotUpdate.String())
} }
return nil return nil
} }
func DeleteOrderItem(oderItem *OrderItem) error { func (c *Controller) deleteOrderItem(oderItem *OrderItem) error {
err := config.Cafe.Database.ORM.Delete(oderItem).Error err := c.orm.Delete(oderItem).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotDelete.String()) return fmt.Errorf(types.CannotDelete.String())
} }

View file

@ -0,0 +1,105 @@
package controller
import (
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
)
// @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} Bill
// @Router /bills [get]
func (c *Controller) GetBills(ctx echo.Context) error {
year := ctx.QueryParam("year")
month := ctx.QueryParam("month")
day := ctx.QueryParam("day")
if year == "" || month == "" || day == "" {
return echo.NewHTTPError(http.StatusInternalServerError, types.MissingInformation.String())
}
bills, err := c.GetAllBills(year, month, day)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.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} BillItem
// @Router /bills/items [get]
func (c *Controller) GetBillItems(ctx echo.Context) error {
bill, err := c.DoesBillExist(ctx.QueryParam("bill"))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
billItems, err := c.GetAllBillItems(bill.ID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.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} Bill
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /bills [post]
func (c *Controller) CreateBill(ctx echo.Context) error {
table, tableErr := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
if tableErr != nil {
return echo.NewHTTPError(http.StatusBadRequest, types.MissingInformation.String())
}
stringFiler := ctx.QueryParam("filter")
var filter []string
if stringFiler != "" {
filter = strings.Split(stringFiler, ",")
}
bill, err := c.createBill(GetOrderOptions{TableId: table, Grouped: true, Filter: filter})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.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 "Internal Server Error"
// @Router /bills/{id} [delete]
func (c *Controller) DeleteBill(ctx echo.Context) error {
id := ctx.Param("id")
bill, err := c.DoesBillExist(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.deleteBill(&bill)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -0,0 +1,209 @@
package controller
import (
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
)
// @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} Order
// @Router /orders [get]
func (c *Controller) GetOrders(ctx echo.Context) error {
table, _ := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
grouping, _ := strconv.ParseBool(ctx.QueryParam("grouping"))
stringFiler := ctx.QueryParam("filter")
var filter []string
if stringFiler != "" {
filter = strings.Split(stringFiler, ",")
}
options := GetOrderOptions{TableId: table, Grouped: grouping, Filter: filter}
var orders []Order
if options.TableId == 0 {
orders = c.getAllActiveOrders()
} else {
orders = c.getAllOrdersForTable(options)
}
return ctx.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} Order
// @Failure 400
// @Failure 500 "Internal Server Error"
// @Router /orders [post]
func (c *Controller) CreateOrder(ctx echo.Context) error {
table, err1 := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
item, err2 := strconv.ParseUint(ctx.QueryParam("item"), 10, 64)
if err1 != nil || err2 != nil {
return echo.NewHTTPError(http.StatusBadRequest, types.MissingInformation.String())
}
order := Order{TableID: table, OrderItemID: item, IsServed: false}
err := c.createOrder(&order)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.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 "Bad Request"
// @Failure 500 "Internal Server Error"
// @Router /orders [delete]
func (c *Controller) DeleteOrder(ctx echo.Context) error {
item := ctx.QueryParam("item")
table := ctx.QueryParam("table")
if table == "" || item == "" {
return echo.NewHTTPError(http.StatusBadRequest, types.MissingInformation.String())
}
err := c.deleteOrder(table, item)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}
// @Schemes
// @Summary update an order
// @Description updates an order with provided information
// @Tags orders
// @Accept json
// @Produce json
// @Param order body Order true "updated Order"
// @Success 200 {object} Order
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /orders [put]
func (c *Controller) UpdateOrder(ctx echo.Context) error {
var newOrder Order
err := ctx.Bind(&newOrder)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldOrder, err := c.doesOrderExist(strconv.Itoa(int(newOrder.ID)))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateOrder(&oldOrder, &newOrder)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, newOrder)
}
// @Schemes
// @Summary get all orderItems
// @Description gets all orderItems as array
// @Tags orderItems
// @Produce json
// @Param type query int true "ItemType"
// @Success 200 {array} OrderItem
// @Router /orders/items [get]
func (c *Controller) GetOrderItems(ctx echo.Context) error {
orderType := ctx.QueryParam("type")
if orderType == "" {
return echo.NewHTTPError(http.StatusBadRequest, types.MissingInformation.String())
}
return ctx.JSON(http.StatusOK, c.getOrderItemsForType(orderType))
}
// @Schemes
// @Summary create new orderItem
// @Description creates a new orderItem and returns it
// @Tags orderItems
// @Accept json
// @Produce json
// @Param order body OrderItem true "OrderItem ID"
// @Success 201 {object} OrderItem
// @Failure 400 "Bad Request"
// @Failure 500 "Internal Server Error"
// @Router /orders/items [post]
func (c *Controller) CreateOrderItem(ctx echo.Context) error {
var orderItem OrderItem
err := ctx.Bind(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
err = c.createOrderItem(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.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 OrderItem true "updated OrderItem"
// @Success 200 {object} OrderItem
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /orders/items [put]
func (c *Controller) UpdateOrderItem(ctx echo.Context) error {
var newOrderItem OrderItem
err := ctx.Bind(&newOrderItem)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldOrderItem, err := c.doesOrderItemExist(strconv.Itoa(int(newOrderItem.ID)))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateOrderItem(&oldOrderItem, &newOrderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.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 "Internal Server Error"
// @Router /orders/items/{id} [delete]
func (c *Controller) DeleteOrderItem(ctx echo.Context) error {
id := ctx.Param("id")
orderItem, err := c.doesOrderItemExist(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.deleteOrderItem(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -0,0 +1,51 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
)
// @Schemes
// @Summary get all active tables
// @Description gets all active tables as array
// @Tags tables
// @Produce json
// @Success 200 {array} Table
// @Router /tables [get]
func (c *Controller) GetTables(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, c.GetAllTables())
}
// @Schemes
// @Summary create new table
// @Description creates a new table and returns it
// @Tags tables
// @Accept json
// @Produce json
// @Success 201 {object} Table "Table has been created"
// @Failure 500 "Internal Server Error"
// @Router /tables [post]
func (c *Controller) CreateTable(ctx echo.Context) error {
table, err := c.CreateNewTable()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.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 "Internal Server Error"
// @Router /tables [delete]
func (c *Controller) DeleteTable(ctx echo.Context) error {
err := c.DeleteLatestTable()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -0,0 +1,54 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
)
// @Schemes
// @Summary get a user
// @Description gets a user
// @Tags users
// @Produce json
// @Param username path string true "Username"
// @Success 200 {object} User
// @Failure 500 "Internal Server Error"
// @Router /users/{username} [get]
func (c *Controller) GetUser(ctx echo.Context) error {
username := ctx.Param("username")
u, err := c.getUserOrCreate(username)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return ctx.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 true "updated User"
// @Success 200 {object} User
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /users [put]
func (c *Controller) UpdateUser(ctx echo.Context) error {
var newUser User
err := ctx.Bind(&newUser)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldUser, err := c.doesUserExist(newUser.Username)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateUser(&oldUser, &newUser)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, newUser)
}

View file

@ -1,10 +1,9 @@
package service package controller
import ( import (
"cafe/config"
"cafe/types"
"fmt" "fmt"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
"gorm.io/plugin/soft_delete" "gorm.io/plugin/soft_delete"
) )
@ -16,9 +15,9 @@ type Table struct {
IsDeleted soft_delete.DeletedAt `gorm:"softDelete:flag" json:"is_deleted" swaggerignore:"true"` IsDeleted soft_delete.DeletedAt `gorm:"softDelete:flag" json:"is_deleted" swaggerignore:"true"`
} }
func GetAllTables() []Table { func (c *Controller) GetAllTables() []Table {
var tables []Table var tables []Table
config.Cafe.Database.ORM.Model( c.orm.Model(
&Table{}, &Table{},
).Joins( ).Joins(
"left join orders on tables.id = orders.table_id", "left join orders on tables.id = orders.table_id",
@ -32,15 +31,15 @@ func GetAllTables() []Table {
return tables return tables
} }
func CreateNewTable() (Table, error) { func (c *Controller) CreateNewTable() (Table, error) {
var table Table var table Table
var err error var err error
result := config.Cafe.Database.ORM.Unscoped().Where("is_deleted = ?", 1).Limit(1).Find(&table) result := c.orm.Unscoped().Where("is_deleted = ?", 1).Limit(1).Find(&table)
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
err = config.Cafe.Database.ORM.Create(&table).Error err = c.orm.Create(&table).Error
} else { } else {
table.IsDeleted = 0 table.IsDeleted = 0
err = config.Cafe.Database.ORM.Unscoped().Save(&table).Error err = c.orm.Unscoped().Save(&table).Error
} }
if err != nil { if err != nil {
return table, fmt.Errorf(types.CannotCreate.String()) return table, fmt.Errorf(types.CannotCreate.String())
@ -48,9 +47,9 @@ func CreateNewTable() (Table, error) {
return table, nil return table, nil
} }
func DeleteLatestTable() error { func (c *Controller) DeleteLatestTable() error {
var table Table var table Table
err := config.Cafe.Database.ORM.Model( err := c.orm.Model(
&Table{}, &Table{},
).Joins( ).Joins(
"left join orders on tables.id = orders.table_id", "left join orders on tables.id = orders.table_id",
@ -67,7 +66,7 @@ func DeleteLatestTable() error {
if table.OrderCount != 0 { if table.OrderCount != 0 {
return fmt.Errorf(types.StillInUse.String()) return fmt.Errorf(types.StillInUse.String())
} }
err = config.Cafe.Database.ORM.Delete(&table).Error err = c.orm.Delete(&table).Error
if err != nil { if err != nil {
return fmt.Errorf(types.CannotDelete.String()) return fmt.Errorf(types.CannotDelete.String())
} }

View file

@ -1,9 +1,9 @@
package user package controller
import ( import (
"cafe/config"
"cafe/types"
"fmt" "fmt"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/types"
) )
type User struct { type User struct {
@ -12,26 +12,26 @@ type User struct {
ShowHotDrinks bool `json:"show_hot_drinks" validate:"required"` ShowHotDrinks bool `json:"show_hot_drinks" validate:"required"`
} }
func DoesUserExist(username string) (User, error) { func (c *Controller) doesUserExist(username string) (User, error) {
var user User var user User
result := config.Cafe.Database.ORM.Limit(1).Find(&user, "username = ?", username) result := c.orm.Limit(1).Find(&user, "username = ?", username)
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
return user, fmt.Errorf(types.CannotFind.String()) return user, fmt.Errorf(types.CannotFind.String())
} }
return user, nil return user, nil
} }
func GetUserOrCreate(username string) (User, error) { func (c *Controller) getUserOrCreate(username string) (User, error) {
var user User var user User
err := config.Cafe.Database.ORM.Where(User{Username: username}).Attrs(User{ShowHotDrinks: true, ShowColdDrinks: true}).FirstOrCreate(&user).Error err := c.orm.Where(User{Username: username}).Attrs(User{ShowHotDrinks: true, ShowColdDrinks: true}).FirstOrCreate(&user).Error
if err != nil { if err != nil {
return user, fmt.Errorf(types.CannotCreate.String()) return user, fmt.Errorf(types.CannotCreate.String())
} }
return user, nil return user, nil
} }
func UpdateUser(old *User, new *User) error { func (c *Controller) updateUser(old *User, new *User) error {
err := config.Cafe.Database.ORM.First(old).Updates(map[string]interface{}{ err := c.orm.First(old).Updates(map[string]interface{}{
"Username": new.Username, "Username": new.Username,
"ShowColdDrinks": new.ShowColdDrinks, "ShowColdDrinks": new.ShowColdDrinks,
"ShowHotDrinks": new.ShowHotDrinks}).Error "ShowHotDrinks": new.ShowHotDrinks}).Error

View file

@ -0,0 +1,81 @@
package database
import (
"fmt"
"net"
"os"
"time"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"moul.io/zapgorm2"
)
type Database struct {
Host string
User string
Password string
Database string
}
const Storage = "storage/"
func init() {
os.Mkdir(Storage, os.ModePerm)
}
func (d *Database) tryDbConnection() {
i := 1
total := 20
for i <= total {
ln, err := net.DialTimeout("tcp", d.Host, 1*time.Second)
if err != nil {
if i == total {
zap.L().Fatal("Failed connecting to database", zap.Int("attempt", i))
}
zap.L().Warn("Connecting to database", zap.Int("attempt", i))
time.Sleep(2 * time.Second)
i++
} else {
_ = ln.Close()
zap.L().Info("Connected to database", zap.Int("attempt", i))
i = total + 1
}
}
}
func (d *Database) initializeMySql(conf *gorm.Config) *gorm.DB {
var err error
d.tryDbConnection()
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
d.User,
d.Password,
d.Host,
d.Database,
)
orm, err := gorm.Open(mysql.Open(dsn), conf)
if err != nil {
zap.L().Error(err.Error())
}
return orm
}
func (d *Database) initializeSqLite(conf *gorm.Config) *gorm.DB {
var err error
orm, err := gorm.Open(sqlite.Open(Storage+"db.sqlite?_pragma=foreign_keys(1)"), conf)
if err != nil {
zap.L().Error(err.Error())
}
return orm
}
func NewDatabaseConnection(d *Database) *gorm.DB {
logger := zapgorm2.New(zap.L())
conf := &gorm.Config{Logger: logger, PrepareStmt: true}
if d.Host == "" {
return d.initializeSqLite(conf)
}
return d.initializeMySql(conf)
}

70
internal/env/env.go vendored Normal file
View file

@ -0,0 +1,70 @@
package env
import (
"errors"
"fmt"
"os"
"github.com/caarlos0/env/v8"
"github.com/containrrr/shoutrrr"
"github.com/go-playground/validator/v10"
)
type Config struct {
TimeZone string `env:"TZ" envDefault:"Etc/UTC" validate:"timezone"`
Port int `env:"PORT" envDefault:"8080" validate:"min=1024,max=49151"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info" validate:"oneof=debug info warn error panic fatal"`
Version string `env:"APP_VERSION" envDefault:"v0.0.0"`
SwaggerHost string `env:"SWAGGER_HOST" envDefault:"https://cafe.test"`
DB_Host string `env:"DB_HOST"`
DB_User string `env:"DB_USER"`
DB_Password string `env:"DB_PASSWORD"`
DB_Database string `env:"DB_DATABASE"`
}
var errParse = errors.New("error parsing environment variables")
func Parse() (*Config, error) {
cfg := &Config{}
if err := env.Parse(cfg); err != nil {
return cfg, err
}
if err := validateContent(cfg); err != nil {
return cfg, err
}
setAllDefaultEnvs(cfg)
return cfg, nil
}
func newEnvValidator() *validator.Validate {
validate := validator.New()
validate.RegisterValidation(`shoutrrr`, func(fl validator.FieldLevel) bool {
value := fl.Field().Interface().(string)
_, err := shoutrrr.CreateSender(value)
return err == nil
})
return validate
}
func validateContent(cfg *Config) error {
validate := newEnvValidator()
err := validate.Struct(cfg)
if err != nil {
if _, ok := err.(*validator.InvalidValidationError); ok {
return err
} else {
for _, err := range err.(validator.ValidationErrors) {
return err
}
}
return errParse
}
return nil
}
func setAllDefaultEnvs(cfg *Config) {
os.Setenv("TZ", cfg.TimeZone)
os.Setenv("PORT", fmt.Sprintf("%d", cfg.Port))
os.Setenv("LOG_LEVEL", cfg.LogLevel)
}

56
internal/env/env_test.go vendored Normal file
View file

@ -0,0 +1,56 @@
package env
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPortParser(t *testing.T) {
key := "PORT"
var err error
defer func() {
os.Unsetenv(key)
}()
os.Setenv(key, "1024")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "-12")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.Port' Error:Field validation for 'Port' failed on the 'min' tag", "Validation should fail")
os.Setenv(key, "60000")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.Port' Error:Field validation for 'Port' failed on the 'max' tag", "Validation should fail")
os.Setenv(key, "abc")
_, err = Parse()
assert.Equal(t, err.Error(), "env: parse error on field \"Port\" of type \"int\": strconv.ParseInt: parsing \"abc\": invalid syntax", "Parsing should fail")
}
func TestTimeZoneParser(t *testing.T) {
key := "TZ"
var err error
defer func() {
os.Unsetenv(key)
}()
os.Setenv(key, "Europe/Berlin")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "Etc/UTC")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "abc")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.TimeZone' Error:Field validation for 'TimeZone' failed on the 'timezone' tag", "Validation should fail")
os.Setenv(key, "-1")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.TimeZone' Error:Field validation for 'TimeZone' failed on the 'timezone' tag", "Validation should fail")
}

View file

@ -0,0 +1,44 @@
package logging
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestInfoLogger(t *testing.T) {
level := "info"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestWarnLogger(t *testing.T) {
level := "warn"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestDebugLogger(t *testing.T) {
level := "debug"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestInvalidLogger(t *testing.T) {
level := "invalid"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), "info", "Level should be info")
}

View file

@ -0,0 +1,33 @@
package logging
import (
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func CreateLogger(logLevel string) *zap.Logger {
encoderCfg := zap.NewProductionEncoderConfig()
encoderCfg.TimeKey = "time"
encoderCfg.EncodeTime = zapcore.TimeEncoderOfLayout(time.StampMilli)
level := zap.NewAtomicLevelAt(zap.InfoLevel)
zapLevel, err := zap.ParseAtomicLevel(logLevel)
if err == nil {
level = zapLevel
}
config := zap.Config{
Level: level,
Development: false,
DisableCaller: false,
DisableStacktrace: false,
Sampling: nil,
Encoding: "json",
EncoderConfig: encoderCfg,
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
}
return zap.Must(config.Build())
}

View file

@ -0,0 +1,13 @@
package router
import (
"github.com/labstack/echo/v4"
)
func authHeader(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("Remote-Groups", c.Request().Header.Get("Remote-Groups"))
c.Response().Header().Set("Remote-Name", c.Request().Header.Get("Remote-Name"))
return next(c)
}
}

94
internal/router/router.go Normal file
View file

@ -0,0 +1,94 @@
package router
import (
"net/http"
"net/url"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/docs"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/controller"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/env"
"go.uber.org/zap"
)
func InitRouter() *echo.Echo {
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.Use(middleware.Recover())
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Skipper: func(c echo.Context) bool {
return strings.Contains(c.Request().URL.Path, "swagger")
},
}))
e.Pre(middleware.RemoveTrailingSlash())
e.Validator = &CustomValidator{Validator: newValidator()}
return e
}
func SetupRoutes(e *echo.Echo, ctrl *controller.Controller, env *env.Config) {
api := e.Group("/api")
{
tableGroup := api.Group("/tables")
{
tableGroup.GET("", ctrl.GetTables)
tableGroup.POST("", ctrl.CreateTable)
tableGroup.DELETE("", ctrl.DeleteTable)
}
orderGroup := api.Group("/orders")
{
orderGroup.GET("", ctrl.GetOrders)
orderGroup.POST("", ctrl.CreateOrder)
orderGroup.DELETE("", ctrl.DeleteOrder)
orderGroup.PUT("", ctrl.UpdateOrder)
orderGroup.GET("/sse", echo.WrapHandler(http.HandlerFunc(ctrl.SSE.ServeHTTP)))
orderItemGroup := orderGroup.Group("/items")
{
orderItemGroup.GET("", ctrl.GetOrderItems)
orderItemGroup.POST("", ctrl.CreateOrderItem)
orderItemGroup.PUT("", ctrl.UpdateOrderItem)
orderItemGroup.DELETE("/:id", ctrl.DeleteOrderItem)
}
}
billGroup := api.Group("/bills")
{
billGroup.GET("", ctrl.GetBills)
billGroup.POST("", ctrl.CreateBill)
billGroup.DELETE("/:id", ctrl.DeleteBill)
billItemGroup := billGroup.Group("/items")
{
billItemGroup.GET("", ctrl.GetBillItems)
}
}
userGroup := api.Group("/users")
{
userGroup.GET("/:username", ctrl.GetUser)
userGroup.PUT("", ctrl.UpdateUser)
}
health := api.Group("/health", authHeader)
{
health.GET("", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, ".")
})
}
if env.SwaggerHost != "" {
docs.SwaggerInfo.Title = "Cafe"
docs.SwaggerInfo.Description = "This is the backend of a cafe"
docs.SwaggerInfo.Version = env.Version
docs.SwaggerInfo.BasePath = "/api"
parsed, _ := url.Parse(env.SwaggerHost)
docs.SwaggerInfo.Host = parsed.Host
api.GET("/swagger/*", echoSwagger.WrapHandler)
zap.L().Info("swagger running", zap.String("url", env.SwaggerHost+"/api/swagger/index.html"))
}
}
}

View file

@ -0,0 +1,17 @@
package router
import (
"github.com/go-playground/validator/v10"
)
type CustomValidator struct {
Validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.Validator.Struct(i)
}
func newValidator() *validator.Validate {
return validator.New()
}

39
main.go
View file

@ -1,39 +0,0 @@
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")
}
}

View file

@ -7,7 +7,10 @@ case $action in
go install github.com/swaggo/swag/cmd/swag@latest go install github.com/swaggo/swag/cmd/swag@latest
;; ;;
"init") "init")
swag init -g api/swagger.go swag init --dir internal/controller -g ../router/router.go -pd
;;
"format")
swag fmt
;; ;;
*) *)
exit 0 exit 0

View file

@ -1,18 +0,0 @@
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)
}

View file

@ -1,12 +0,0 @@
<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>

View file

@ -1,23 +0,0 @@
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)
}

View file

@ -1,36 +0,0 @@
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
}
}
}