diff --git a/.air.toml b/.air.toml index 498951f..2b02fcc 100644 --- a/.air.toml +++ b/.air.toml @@ -5,9 +5,9 @@ tmp_dir = "tmp" [build] args_bin = [] bin = "./tmp/main" - cmd = "go build -o ./tmp/main ." + cmd = "go build -o ./tmp/main ./cmd/api" delay = 1000 - exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_dir = ["assets", "tmp", "vendor", "testdata", "query"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..d776acd --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "log" + "log/slog" + "os" + + "git.kocoder.xyz/kocoded/vt/routers" + "git.kocoder.xyz/kocoded/vt/utils" + "github.com/gofiber/contrib/websocket" + "github.com/gofiber/fiber/v2" + "github.com/joho/godotenv" +) + +func main() { + + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + err := godotenv.Load() + if err != nil { + logger.Error("Error Loading Environment variables! ", "error", err) + } + + db := utils.SetupDatabase(os.Getenv("DB_DSN"), logger) + + appCtx := &utils.Application{Logger: logger, DB: db} + + app := fiber.New() + + utils.RegisterMiddlewares(app) + + utils.CreateOIDCClient(context.Background(), app, logger) + + routers.RegisterMandantRouter(app.Group("/v1/mandant"), appCtx) + routers.RegisterAnsprechpartnerRouter(app.Group("/v1/ansprechpartner"), appCtx) + routers.RegisterFirmaRouter(app.Group("/v1/firma"), appCtx) + + app.Use("/ws", func(c *fiber.Ctx) error { + if websocket.IsWebSocketUpgrade(c) { + c.Locals("allowed", true) + return c.Next() + } + return fiber.ErrUpgradeRequired + }) + + app.Get("/ws/:id", websocket.New(func(c *websocket.Conn) { + log.Println(c.Locals("allowed")) // true + log.Println(c.Params("id")) // 123 + log.Println(c.Query("v")) // 1.0 + log.Println(c.Cookies("session")) // "" + + var ( + mt int + msg []byte + err error + ) + for { + if mt, msg, err = c.ReadMessage(); err != nil { + slog.Info("read", "error", err) + break + } + + slog.Info("recv:", "messageType", mt, "bytes", msg) + + if err = c.WriteMessage(mt, msg); err != nil { + slog.Info("write", "error", err) + break + } + } + })) + + log.Fatalln(app.Listen(":3000")) +} diff --git a/go.mod b/go.mod index 899dbe6..79fe1ef 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,16 @@ require ( require ( github.com/fasthttp/websocket v1.5.8 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect golang.org/x/net v0.43.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect ) require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect + github.com/coreos/go-oidc/v3 v3.15.0 github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/gofiber/contrib/websocket v1.3.4 github.com/google/uuid v1.6.0 // indirect @@ -28,6 +31,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 484c752..3877d85 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,15 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg= +github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= 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/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8= github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/gofiber/contrib/websocket v1.3.4 h1:tWeBdbJ8q0WFQXariLN4dBIbGH9KBU75s0s7YXplOSg= @@ -35,6 +39,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -88,6 +94,8 @@ golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go deleted file mode 100644 index 65e5c2e..0000000 --- a/main.go +++ /dev/null @@ -1,147 +0,0 @@ -package main - -import ( - "errors" - "log" - "log/slog" - "os" - "strconv" - - "git.kocoder.xyz/kocoded/vt/model" - "git.kocoder.xyz/kocoded/vt/query" - "git.kocoder.xyz/kocoded/vt/routers" - "git.kocoder.xyz/kocoded/vt/utils" - "github.com/gofiber/contrib/websocket" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/compress" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/helmet" - "github.com/gofiber/fiber/v2/middleware/idempotency" - "github.com/gofiber/fiber/v2/middleware/logger" - "github.com/gofiber/fiber/v2/middleware/monitor" - "github.com/gofiber/fiber/v2/middleware/pprof" - "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/gofiber/fiber/v2/middleware/requestid" - "github.com/gofiber/fiber/v2/middleware/skip" - "gorm.io/driver/postgres" - "gorm.io/gen" - "gorm.io/gorm" -) - -func main() { - logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) - - SetupDatabase() - - app := fiber.New() - - RegisterMiddlewares(app) - - routers.RegisterAnsprechpartnerRouter(app.Group("/v1/ansprechpartner"), logger) - routers.RegisterFirmaRouter(app.Group("/v1/firma"), logger) - - app.Use("/ws", func(c *fiber.Ctx) error { - if websocket.IsWebSocketUpgrade(c) { - c.Locals("allowed", true) - return c.Next() - } - return fiber.ErrUpgradeRequired - }) - app.Get("/ws/:id", websocket.New(func(c *websocket.Conn) { - log.Println(c.Locals("allowed")) // true - log.Println(c.Params("id")) // 123 - log.Println(c.Query("v")) // 1.0 - log.Println(c.Cookies("session")) // "" - - var ( - mt int - msg []byte - err error - ) - for { - if mt, msg, err = c.ReadMessage(); err != nil { - slog.Info("read", "error", err) - break - } - - slog.Info("recv:", "messageType", mt, "bytes", msg) - - if err = c.WriteMessage(mt, msg); err != nil { - slog.Info("write", "error", err) - break - } - } - })) - - log.Fatalln(app.Listen(":3000")) -} - -func RegisterMiddlewares(app *fiber.App) { - app.Use(requestid.New()) - app.Use(compress.New()) - app.Use(helmet.New()) - app.Use(cors.New()) - // app.Use(csrf.New()) - // app.Use(healthcheck.New(healthcheck.Config{})) - app.Use(idempotency.New()) - // app.Use(limiter.New()) - app.Use(logger.New()) - app.Use("/dbg/monitor", monitor.New()) - app.Use(pprof.New()) - app.Use(recover.New()) - app.Use(skip.New(AddPaginationParams, func(c *fiber.Ctx) bool { - return c.Method() != fiber.MethodGet - })) -} - -func AddPaginationParams(c *fiber.Ctx) error { - err := c.Next() - if err != nil { - var offset *utils.OffsetPaginationError - if errors.As(err, &offset) { - c.Append("X-Page", strconv.Itoa(offset.Page)) - c.Append("X-Pages", strconv.Itoa(offset.Pages)) - c.Append("X-Next-Page", strconv.Itoa(offset.NextPage)) - c.Append("X-Last-Page", strconv.Itoa(offset.LastPage)) - return nil - } - - var keyset *utils.KeysetPaginationError - if errors.As(err, &keyset) { - c.Append("X-Key", strconv.Itoa(keyset.Key)) - c.Append("X-Previous-Key", strconv.Itoa(keyset.PreviousKey)) - c.Append("X-Next-Key", strconv.Itoa(keyset.NextKey)) - return nil - } - } - return err -} - -func SetupDatabase() { - dsn := "host=10.1.0.2 user=vt password=20a1c7809cd065bc5afe7c36fde26abf625316c8a83cc841b435c9acf3619b1f dbname=vt port=5432 sslmode=prefer TimeZone=Europe/Vienna" - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - panic(err) - } - - db.SetupJoinTable(model.Ansprechpartner{}, "Firmen", model.FirmaAnsprechpartner{}) - db.SetupJoinTable(model.Firma{}, "Ansprechpartner", model.FirmaAnsprechpartner{}) - - db.AutoMigrate(&model.Ansprechpartner{}, &model.FirmaAnsprechpartner{}, &model.Firma{}) - - g := gen.NewGenerator(gen.Config{ - OutPath: "./query", - Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode - }) - - // gormdb, _ := gorm.Open(mysql.Open("root:@(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local")) - g.UseDB(db) // reuse your gorm db - - // Generate basic type-safe DAO API for struct `model.User` following conventions - g.ApplyBasic(model.Ansprechpartner{}, model.Dokument{}, model.Firma{}, model.Kalender{}, model.Kalendereintrag{}, model.Kostenstelle{}, model.Lager{}, model.Lagerplatz{}, model.Material{}, model.Nachricht{}, model.Projekt{}, model.Rechnung{}, model.Rechnungsposition{}, model.Scanobject{}, model.User{}, model.Zahlung{}, model.FirmaAnsprechpartner{}) - - // Generate the code - g.Execute() - - query.SetDefault(db) -} diff --git a/model/user.go b/model/user.go index 8c1930d..f7c8560 100644 --- a/model/user.go +++ b/model/user.go @@ -4,4 +4,6 @@ import "gorm.io/gorm" type User struct { gorm.Model + Sub string `json:"sub"gorm:"unique"` + Email string } diff --git a/query/users.gen.go b/query/users.gen.go index 8c9a83d..c0b3e51 100644 --- a/query/users.gen.go +++ b/query/users.gen.go @@ -32,6 +32,8 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user { _user.CreatedAt = field.NewTime(tableName, "created_at") _user.UpdatedAt = field.NewTime(tableName, "updated_at") _user.DeletedAt = field.NewField(tableName, "deleted_at") + _user.Sub = field.NewString(tableName, "sub") + _user.Email = field.NewString(tableName, "email") _user.fillFieldMap() @@ -46,6 +48,8 @@ type user struct { CreatedAt field.Time UpdatedAt field.Time DeletedAt field.Field + Sub field.String + Email field.String fieldMap map[string]field.Expr } @@ -66,6 +70,8 @@ func (u *user) updateTableName(table string) *user { u.CreatedAt = field.NewTime(table, "created_at") u.UpdatedAt = field.NewTime(table, "updated_at") u.DeletedAt = field.NewField(table, "deleted_at") + u.Sub = field.NewString(table, "sub") + u.Email = field.NewString(table, "email") u.fillFieldMap() @@ -82,11 +88,13 @@ func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (u *user) fillFieldMap() { - u.fieldMap = make(map[string]field.Expr, 4) + u.fieldMap = make(map[string]field.Expr, 6) u.fieldMap["id"] = u.ID u.fieldMap["created_at"] = u.CreatedAt u.fieldMap["updated_at"] = u.UpdatedAt u.fieldMap["deleted_at"] = u.DeletedAt + u.fieldMap["sub"] = u.Sub + u.fieldMap["email"] = u.Email } func (u user) clone(db *gorm.DB) user { diff --git a/routers/ansprechpartner.go b/routers/ansprechpartner.go index 2ebe5d9..19ee608 100644 --- a/routers/ansprechpartner.go +++ b/routers/ansprechpartner.go @@ -5,6 +5,7 @@ import ( "git.kocoder.xyz/kocoded/vt/model" "git.kocoder.xyz/kocoded/vt/query" + "git.kocoder.xyz/kocoded/vt/utils" "github.com/gofiber/fiber/v2" ) @@ -12,8 +13,8 @@ type ansprechpartnerRouter struct { logger *slog.Logger } -func RegisterAnsprechpartnerRouter(group fiber.Router, logger *slog.Logger) { - router := &ansprechpartnerRouter{logger: logger} +func RegisterAnsprechpartnerRouter(group fiber.Router, appCtx *utils.Application) { + router := &ansprechpartnerRouter{logger: appCtx.Logger} group.Post("/new", router.createAnsprechpartner) group.Get("/all", router.getAllAnsprechpartners) diff --git a/routers/firma.go b/routers/firma.go index 7a5770a..454f292 100644 --- a/routers/firma.go +++ b/routers/firma.go @@ -5,6 +5,7 @@ import ( "git.kocoder.xyz/kocoded/vt/model" "git.kocoder.xyz/kocoded/vt/query" + "git.kocoder.xyz/kocoded/vt/utils" "github.com/gofiber/fiber/v2" ) @@ -12,8 +13,8 @@ type firmaRouter struct { logger *slog.Logger } -func RegisterFirmaRouter(group fiber.Router, logger *slog.Logger) { - router := &firmaRouter{logger: logger} +func RegisterFirmaRouter(group fiber.Router, appCtx *utils.Application) { + router := &firmaRouter{logger: appCtx.Logger} group.Post("/new", router.createFirma) group.Get("/all", router.getAllFirmen) diff --git a/routers/mandant.go b/routers/mandant.go new file mode 100644 index 0000000..ba2936a --- /dev/null +++ b/routers/mandant.go @@ -0,0 +1,79 @@ +package routers + +import ( + "log/slog" + "slices" + + "git.kocoder.xyz/kocoded/vt/utils" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +type Mandant struct { + ID string `json:"id"` + Name string `json:"name"` + Logo string `json:"logo"` + Plan string `json:"plan"` + Color string `json:"color"` +} + +type mandantRouter struct { + logger *slog.Logger + mandanten []*Mandant + currentMandant *Mandant +} + +func RegisterMandantRouter(group fiber.Router, appCtx *utils.Application) { + mandanten := []*Mandant{ + { + ID: uuid.NewString(), + Name: "Acme Inc", + Logo: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvbW1hbmQtaWNvbiBsdWNpZGUtY29tbWFuZCI+PHBhdGggZD0iTTE1IDZ2MTJhMyAzIDAgMSAwIDMtM0g2YTMgMyAwIDEgMCAzIDNWNmEzIDMgMCAxIDAtMyAzaDEyYTMgMyAwIDEgMC0zLTMiLz48L3N2Zz4=", + Plan: "Enterprise", + Color: "#ff2056", + }, + { + ID: uuid.NewString(), + Name: "Acme Corp.", + Logo: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1leWUtY2xvc2VkLWljb24gbHVjaWRlLWV5ZS1jbG9zZWQiPjxwYXRoIGQ9Im0xNSAxOC0uNzIyLTMuMjUiLz48cGF0aCBkPSJNMiA4YTEwLjY0NSAxMC42NDUgMCAwIDAgMjAgMCIvPjxwYXRoIGQ9Im0yMCAxNS0xLjcyNi0yLjA1Ii8+PHBhdGggZD0ibTQgMTUgMS43MjYtMi4wNSIvPjxwYXRoIGQ9Im05IDE4IC43MjItMy4yNSIvPjwvc3ZnPg==", + Plan: "Startup", + Color: "#e12afb", + }, + { + ID: uuid.NewString(), + Name: "Evil Corp.", + Logo: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNvbW1hbmQtaWNvbiBsdWNpZGUtY29tbWFuZCI+PHBhdGggZD0iTTE1IDZ2MTJhMyAzIDAgMSAwIDMtM0g2YTMgMyAwIDEgMCAzIDNWNmEzIDMgMCAxIDAtMyAzaDEyYTMgMyAwIDEgMC0zLTMiLz48L3N2Zz4=", + Plan: "Free", + Color: "#4f39f6", + }, + } + + router := &mandantRouter{logger: appCtx.Logger, mandanten: mandanten, currentMandant: mandanten[0]} + + group.Get("/current", router.getCurrentMandant) + group.Put("/current", router.setCurrentMandant) + group.Get("/all", router.getAllMandant) +} + +func (r *mandantRouter) getCurrentMandant(c *fiber.Ctx) error { + return c.JSON(r.currentMandant) +} + +func (r *mandantRouter) getAllMandant(c *fiber.Ctx) error { + return c.JSON(r.mandanten) +} + +func (r *mandantRouter) setCurrentMandant(c *fiber.Ctx) error { + mandant := &Mandant{} + + if err := c.BodyParser(mandant); err != nil { + return err + } + + mandantId := slices.IndexFunc(r.mandanten, func(m *Mandant) bool { + return m.ID == mandant.ID + }) + + r.currentMandant = r.mandanten[mandantId] + return c.JSON(r.currentMandant) +} diff --git a/utils/applicationCtx.go b/utils/applicationCtx.go new file mode 100644 index 0000000..8a6c9fd --- /dev/null +++ b/utils/applicationCtx.go @@ -0,0 +1,12 @@ +package utils + +import ( + "log/slog" + + "gorm.io/gorm" +) + +type Application struct { + Logger *slog.Logger + DB *gorm.DB +} diff --git a/utils/authentication.go b/utils/authentication.go new file mode 100644 index 0000000..f1ff154 --- /dev/null +++ b/utils/authentication.go @@ -0,0 +1,108 @@ +package utils + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "os" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" + "golang.org/x/oauth2" +) + +func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) { + setCallbackCookieExp(w, r, name, value, int(time.Hour.Seconds())) +} + +func setCallbackCookieExp(w http.ResponseWriter, r *http.Request, name, value string, maxAge int) { + c := &http.Cookie{ + Name: name, + Value: value, + Path: "/", + MaxAge: maxAge, + Secure: r.TLS != nil, + HttpOnly: true, + } + http.SetCookie(w, c) +} + +func CreateOIDCClient(ctx context.Context, app *fiber.App, logger *slog.Logger) { + provider, err := oidc.NewProvider(ctx, "https://keycloak.kocoder.xyz/realms/che") + if err != nil { + logger.Error("Error generating OIDC Provider. ", "error", err) + } + + oauthConfig := oauth2.Config{ + ClientID: os.Getenv("CLIENT_ID"), + ClientSecret: os.Getenv("CLIENT_SECRET"), + RedirectURL: "http://localhost:3000/api/auth/callback", + + Endpoint: provider.Endpoint(), + + Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile", "email"}, + } + + app.Get("/api/auth", adaptor.HTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) { + state, err := RandString(16) + if err != nil { + logger.Warn("Unable to create a state", "error", err) + http.Error(w, "Unable to create a state", http.StatusInternalServerError) + } + + setCallbackCookie(w, r, "state", state) + + http.Redirect(w, r, oauthConfig.AuthCodeURL(state), http.StatusFound) + })) + + app.Get("/api/auth/callback", adaptor.HTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) { + state, err := r.Cookie("state") + if err != nil { + logger.Warn("State cookie not found", "error", err) + http.Error(w, "state not found", http.StatusBadRequest) + return + } + + if r.URL.Query().Get("state") != state.Value { + logger.Warn("State cookie and header not matching", "error", err) + http.Error(w, "states not matching", http.StatusBadRequest) + return + } + + oauth2Token, err := oauthConfig.Exchange(ctx, r.URL.Query().Get("code")) + if err != nil { + logger.Warn("Failed to exchange token", "error", err) + http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError) + return + } + + userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)) + if err != nil { + logger.Warn("failed to get userinfo", "error", err) + http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError) + return + } + + resp := struct { + Token *oauth2.Token + UserInfo *oidc.UserInfo + }{oauth2Token, userInfo} + + data, err := json.MarshalIndent(resp, "", " ") + if err != nil { + logger.Warn("Failed to parse JSON", "error", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + setCallbackCookieExp(w, r, "state", "", -1) + + _, err = w.Write(data) + if err != nil { + logger.Error("Unable to send response", "error", err) + } + })) +} diff --git a/utils/db.go b/utils/db.go new file mode 100644 index 0000000..9f09f32 --- /dev/null +++ b/utils/db.go @@ -0,0 +1,49 @@ +package utils + +import ( + "log/slog" + + "git.kocoder.xyz/kocoded/vt/model" + "git.kocoder.xyz/kocoded/vt/query" + "gorm.io/driver/postgres" + "gorm.io/gen" + "gorm.io/gorm" +) + +func SetupDatabase(dsn string, logger *slog.Logger) *gorm.DB { + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + logger.Error("Error connecting to the Database", "error", err) + } + + err = db.SetupJoinTable(model.Ansprechpartner{}, "Firmen", model.FirmaAnsprechpartner{}) + if err != nil { + logger.Error("Error setting up Join Tables", "error", err) + } + err = db.SetupJoinTable(model.Firma{}, "Ansprechpartner", model.FirmaAnsprechpartner{}) + if err != nil { + logger.Error("Error setting up Join Tables", "error", err) + } + err = db.AutoMigrate(&model.Ansprechpartner{}, &model.FirmaAnsprechpartner{}, &model.Firma{}) + if err != nil { + logger.Error("Error setting up Join Tables", "error", err) + } + + g := gen.NewGenerator(gen.Config{ + OutPath: "./query", + Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode + }) + + // gormdb, _ := gorm.Open(mysql.Open("root:@(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local")) + g.UseDB(db) // reuse your gorm db + + // Generate basic type-safe DAO API for struct `model.User` following conventions + g.ApplyBasic(model.Ansprechpartner{}, model.Dokument{}, model.Firma{}, model.Kalender{}, model.Kalendereintrag{}, model.Kostenstelle{}, model.Lager{}, model.Lagerplatz{}, model.Material{}, model.Nachricht{}, model.Projekt{}, model.Rechnung{}, model.Rechnungsposition{}, model.Scanobject{}, model.User{}, model.Zahlung{}, model.FirmaAnsprechpartner{}) + + // Generate the code + g.Execute() + + query.SetDefault(db) + + return db +} diff --git a/utils/middleware.go b/utils/middleware.go new file mode 100644 index 0000000..5430500 --- /dev/null +++ b/utils/middleware.go @@ -0,0 +1,33 @@ +package utils + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/compress" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/helmet" + "github.com/gofiber/fiber/v2/middleware/idempotency" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/monitor" + "github.com/gofiber/fiber/v2/middleware/pprof" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/gofiber/fiber/v2/middleware/requestid" + "github.com/gofiber/fiber/v2/middleware/skip" +) + +func RegisterMiddlewares(app *fiber.App) { + app.Use(requestid.New()) + app.Use(compress.New()) + app.Use(helmet.New()) + app.Use(cors.New()) + // app.Use(csrf.New()) + // app.Use(healthcheck.New(healthcheck.Config{})) + app.Use(idempotency.New()) + // app.Use(limiter.New()) + app.Use(logger.New()) + app.Use("/dbg/monitor", monitor.New()) + app.Use(pprof.New()) + app.Use(recover.New()) + app.Use(skip.New(AddPaginationParams, func(c *fiber.Ctx) bool { + return c.Method() != fiber.MethodGet + })) +} diff --git a/utils/pagination.go b/utils/pagination.go index af54cc0..e539ae9 100644 --- a/utils/pagination.go +++ b/utils/pagination.go @@ -1,5 +1,12 @@ package utils +import ( + "errors" + "strconv" + + "github.com/gofiber/fiber/v2" +) + type OffsetPaginationError struct { Page int Pages int @@ -35,3 +42,27 @@ func (p *KeysetPaginationError) Error() string { func NewKeysetPaginationError(key int, next int, previous int) error { return &KeysetPaginationError{Key: key, NextKey: next, PreviousKey: previous} } + +func AddPaginationParams(c *fiber.Ctx) error { + err := c.Next() + if err != nil { + + var offset *OffsetPaginationError + if errors.As(err, &offset) { + c.Append("X-Page", strconv.Itoa(offset.Page)) + c.Append("X-Pages", strconv.Itoa(offset.Pages)) + c.Append("X-Next-Page", strconv.Itoa(offset.NextPage)) + c.Append("X-Last-Page", strconv.Itoa(offset.LastPage)) + return nil + } + + var keyset *KeysetPaginationError + if errors.As(err, &keyset) { + c.Append("X-Key", strconv.Itoa(keyset.Key)) + c.Append("X-Previous-Key", strconv.Itoa(keyset.PreviousKey)) + c.Append("X-Next-Key", strconv.Itoa(keyset.NextKey)) + return nil + } + } + return err +} diff --git a/utils/random.go b/utils/random.go new file mode 100644 index 0000000..d7c7684 --- /dev/null +++ b/utils/random.go @@ -0,0 +1,15 @@ +package utils + +import ( + "crypto/rand" + "encoding/base64" + "io" +) + +func RandString(nByte int) (string, error) { + b := make([]byte, nByte) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +}