Compare commits

...

2 Commits

Author SHA1 Message Date
cf82dede3b Bulk commit 2025-08-21 19:35:01 +02:00
372dced0a1 Mandanten, OAuth, Cleanup von der Main Methode. 2025-08-20 16:08:58 +02:00
20 changed files with 901 additions and 160 deletions

View File

@ -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

74
cmd/api/main.go Normal file
View File

@ -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, appCtx)
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<int>", 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"))
}

4
go.mod
View File

@ -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

8
go.sum
View File

@ -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=

147
main.go
View File

@ -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<int>", 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)
}

11
model/mandant.go Normal file
View File

@ -0,0 +1,11 @@
package model
import (
"git.kocoder.xyz/kocoded/vt/types"
"gorm.io/gorm"
)
type Mandant struct {
gorm.Model
types.Mandant
}

View File

@ -4,4 +4,6 @@ import "gorm.io/gorm"
type User struct {
gorm.Model
Sub string `json:"sub" gorm:"unique"`
Email string
}

View File

@ -26,6 +26,7 @@ var (
Kostenstelle *kostenstelle
Lager *lager
Lagerplatz *lagerplatz
Mandant *mandant
Material *material
Nachricht *nachricht
Projekt *projekt
@ -47,6 +48,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Kostenstelle = &Q.Kostenstelle
Lager = &Q.Lager
Lagerplatz = &Q.Lagerplatz
Mandant = &Q.Mandant
Material = &Q.Material
Nachricht = &Q.Nachricht
Projekt = &Q.Projekt
@ -69,6 +71,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Kostenstelle: newKostenstelle(db, opts...),
Lager: newLager(db, opts...),
Lagerplatz: newLagerplatz(db, opts...),
Mandant: newMandant(db, opts...),
Material: newMaterial(db, opts...),
Nachricht: newNachricht(db, opts...),
Projekt: newProjekt(db, opts...),
@ -92,6 +95,7 @@ type Query struct {
Kostenstelle kostenstelle
Lager lager
Lagerplatz lagerplatz
Mandant mandant
Material material
Nachricht nachricht
Projekt projekt
@ -116,6 +120,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Kostenstelle: q.Kostenstelle.clone(db),
Lager: q.Lager.clone(db),
Lagerplatz: q.Lagerplatz.clone(db),
Mandant: q.Mandant.clone(db),
Material: q.Material.clone(db),
Nachricht: q.Nachricht.clone(db),
Projekt: q.Projekt.clone(db),
@ -147,6 +152,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Kostenstelle: q.Kostenstelle.replaceDB(db),
Lager: q.Lager.replaceDB(db),
Lagerplatz: q.Lagerplatz.replaceDB(db),
Mandant: q.Mandant.replaceDB(db),
Material: q.Material.replaceDB(db),
Nachricht: q.Nachricht.replaceDB(db),
Projekt: q.Projekt.replaceDB(db),
@ -168,6 +174,7 @@ type queryCtx struct {
Kostenstelle IKostenstelleDo
Lager ILagerDo
Lagerplatz ILagerplatzDo
Mandant IMandantDo
Material IMaterialDo
Nachricht INachrichtDo
Projekt IProjektDo
@ -189,6 +196,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Kostenstelle: q.Kostenstelle.WithContext(ctx),
Lager: q.Lager.WithContext(ctx),
Lagerplatz: q.Lagerplatz.WithContext(ctx),
Mandant: q.Mandant.WithContext(ctx),
Material: q.Material.WithContext(ctx),
Nachricht: q.Nachricht.WithContext(ctx),
Projekt: q.Projekt.WithContext(ctx),

411
query/mandants.gen.go Normal file
View File

@ -0,0 +1,411 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import (
"context"
"database/sql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"git.kocoder.xyz/kocoded/vt/model"
)
func newMandant(db *gorm.DB, opts ...gen.DOOption) mandant {
_mandant := mandant{}
_mandant.mandantDo.UseDB(db, opts...)
_mandant.mandantDo.UseModel(&model.Mandant{})
tableName := _mandant.mandantDo.TableName()
_mandant.ALL = field.NewAsterisk(tableName)
_mandant.ID = field.NewUint(tableName, "id")
_mandant.CreatedAt = field.NewTime(tableName, "created_at")
_mandant.UpdatedAt = field.NewTime(tableName, "updated_at")
_mandant.DeletedAt = field.NewField(tableName, "deleted_at")
_mandant.Name = field.NewString(tableName, "name")
_mandant.Logo = field.NewString(tableName, "logo")
_mandant.Plan = field.NewString(tableName, "plan")
_mandant.Color = field.NewString(tableName, "color")
_mandant.fillFieldMap()
return _mandant
}
type mandant struct {
mandantDo
ALL field.Asterisk
ID field.Uint
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Name field.String
Logo field.String
Plan field.String
Color field.String
fieldMap map[string]field.Expr
}
func (m mandant) Table(newTableName string) *mandant {
m.mandantDo.UseTable(newTableName)
return m.updateTableName(newTableName)
}
func (m mandant) As(alias string) *mandant {
m.mandantDo.DO = *(m.mandantDo.As(alias).(*gen.DO))
return m.updateTableName(alias)
}
func (m *mandant) updateTableName(table string) *mandant {
m.ALL = field.NewAsterisk(table)
m.ID = field.NewUint(table, "id")
m.CreatedAt = field.NewTime(table, "created_at")
m.UpdatedAt = field.NewTime(table, "updated_at")
m.DeletedAt = field.NewField(table, "deleted_at")
m.Name = field.NewString(table, "name")
m.Logo = field.NewString(table, "logo")
m.Plan = field.NewString(table, "plan")
m.Color = field.NewString(table, "color")
m.fillFieldMap()
return m
}
func (m *mandant) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := m.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (m *mandant) fillFieldMap() {
m.fieldMap = make(map[string]field.Expr, 8)
m.fieldMap["id"] = m.ID
m.fieldMap["created_at"] = m.CreatedAt
m.fieldMap["updated_at"] = m.UpdatedAt
m.fieldMap["deleted_at"] = m.DeletedAt
m.fieldMap["name"] = m.Name
m.fieldMap["logo"] = m.Logo
m.fieldMap["plan"] = m.Plan
m.fieldMap["color"] = m.Color
}
func (m mandant) clone(db *gorm.DB) mandant {
m.mandantDo.ReplaceConnPool(db.Statement.ConnPool)
return m
}
func (m mandant) replaceDB(db *gorm.DB) mandant {
m.mandantDo.ReplaceDB(db)
return m
}
type mandantDo struct{ gen.DO }
type IMandantDo interface {
gen.SubQuery
Debug() IMandantDo
WithContext(ctx context.Context) IMandantDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IMandantDo
WriteDB() IMandantDo
As(alias string) gen.Dao
Session(config *gorm.Session) IMandantDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IMandantDo
Not(conds ...gen.Condition) IMandantDo
Or(conds ...gen.Condition) IMandantDo
Select(conds ...field.Expr) IMandantDo
Where(conds ...gen.Condition) IMandantDo
Order(conds ...field.Expr) IMandantDo
Distinct(cols ...field.Expr) IMandantDo
Omit(cols ...field.Expr) IMandantDo
Join(table schema.Tabler, on ...field.Expr) IMandantDo
LeftJoin(table schema.Tabler, on ...field.Expr) IMandantDo
RightJoin(table schema.Tabler, on ...field.Expr) IMandantDo
Group(cols ...field.Expr) IMandantDo
Having(conds ...gen.Condition) IMandantDo
Limit(limit int) IMandantDo
Offset(offset int) IMandantDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IMandantDo
Unscoped() IMandantDo
Create(values ...*model.Mandant) error
CreateInBatches(values []*model.Mandant, batchSize int) error
Save(values ...*model.Mandant) error
First() (*model.Mandant, error)
Take() (*model.Mandant, error)
Last() (*model.Mandant, error)
Find() ([]*model.Mandant, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Mandant, err error)
FindInBatches(result *[]*model.Mandant, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*model.Mandant) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IMandantDo
Assign(attrs ...field.AssignExpr) IMandantDo
Joins(fields ...field.RelationField) IMandantDo
Preload(fields ...field.RelationField) IMandantDo
FirstOrInit() (*model.Mandant, error)
FirstOrCreate() (*model.Mandant, error)
FindByPage(offset int, limit int) (result []*model.Mandant, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Rows() (*sql.Rows, error)
Row() *sql.Row
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IMandantDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
func (m mandantDo) Debug() IMandantDo {
return m.withDO(m.DO.Debug())
}
func (m mandantDo) WithContext(ctx context.Context) IMandantDo {
return m.withDO(m.DO.WithContext(ctx))
}
func (m mandantDo) ReadDB() IMandantDo {
return m.Clauses(dbresolver.Read)
}
func (m mandantDo) WriteDB() IMandantDo {
return m.Clauses(dbresolver.Write)
}
func (m mandantDo) Session(config *gorm.Session) IMandantDo {
return m.withDO(m.DO.Session(config))
}
func (m mandantDo) Clauses(conds ...clause.Expression) IMandantDo {
return m.withDO(m.DO.Clauses(conds...))
}
func (m mandantDo) Returning(value interface{}, columns ...string) IMandantDo {
return m.withDO(m.DO.Returning(value, columns...))
}
func (m mandantDo) Not(conds ...gen.Condition) IMandantDo {
return m.withDO(m.DO.Not(conds...))
}
func (m mandantDo) Or(conds ...gen.Condition) IMandantDo {
return m.withDO(m.DO.Or(conds...))
}
func (m mandantDo) Select(conds ...field.Expr) IMandantDo {
return m.withDO(m.DO.Select(conds...))
}
func (m mandantDo) Where(conds ...gen.Condition) IMandantDo {
return m.withDO(m.DO.Where(conds...))
}
func (m mandantDo) Order(conds ...field.Expr) IMandantDo {
return m.withDO(m.DO.Order(conds...))
}
func (m mandantDo) Distinct(cols ...field.Expr) IMandantDo {
return m.withDO(m.DO.Distinct(cols...))
}
func (m mandantDo) Omit(cols ...field.Expr) IMandantDo {
return m.withDO(m.DO.Omit(cols...))
}
func (m mandantDo) Join(table schema.Tabler, on ...field.Expr) IMandantDo {
return m.withDO(m.DO.Join(table, on...))
}
func (m mandantDo) LeftJoin(table schema.Tabler, on ...field.Expr) IMandantDo {
return m.withDO(m.DO.LeftJoin(table, on...))
}
func (m mandantDo) RightJoin(table schema.Tabler, on ...field.Expr) IMandantDo {
return m.withDO(m.DO.RightJoin(table, on...))
}
func (m mandantDo) Group(cols ...field.Expr) IMandantDo {
return m.withDO(m.DO.Group(cols...))
}
func (m mandantDo) Having(conds ...gen.Condition) IMandantDo {
return m.withDO(m.DO.Having(conds...))
}
func (m mandantDo) Limit(limit int) IMandantDo {
return m.withDO(m.DO.Limit(limit))
}
func (m mandantDo) Offset(offset int) IMandantDo {
return m.withDO(m.DO.Offset(offset))
}
func (m mandantDo) Scopes(funcs ...func(gen.Dao) gen.Dao) IMandantDo {
return m.withDO(m.DO.Scopes(funcs...))
}
func (m mandantDo) Unscoped() IMandantDo {
return m.withDO(m.DO.Unscoped())
}
func (m mandantDo) Create(values ...*model.Mandant) error {
if len(values) == 0 {
return nil
}
return m.DO.Create(values)
}
func (m mandantDo) CreateInBatches(values []*model.Mandant, batchSize int) error {
return m.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (m mandantDo) Save(values ...*model.Mandant) error {
if len(values) == 0 {
return nil
}
return m.DO.Save(values)
}
func (m mandantDo) First() (*model.Mandant, error) {
if result, err := m.DO.First(); err != nil {
return nil, err
} else {
return result.(*model.Mandant), nil
}
}
func (m mandantDo) Take() (*model.Mandant, error) {
if result, err := m.DO.Take(); err != nil {
return nil, err
} else {
return result.(*model.Mandant), nil
}
}
func (m mandantDo) Last() (*model.Mandant, error) {
if result, err := m.DO.Last(); err != nil {
return nil, err
} else {
return result.(*model.Mandant), nil
}
}
func (m mandantDo) Find() ([]*model.Mandant, error) {
result, err := m.DO.Find()
return result.([]*model.Mandant), err
}
func (m mandantDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.Mandant, err error) {
buf := make([]*model.Mandant, 0, batchSize)
err = m.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (m mandantDo) FindInBatches(result *[]*model.Mandant, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return m.DO.FindInBatches(result, batchSize, fc)
}
func (m mandantDo) Attrs(attrs ...field.AssignExpr) IMandantDo {
return m.withDO(m.DO.Attrs(attrs...))
}
func (m mandantDo) Assign(attrs ...field.AssignExpr) IMandantDo {
return m.withDO(m.DO.Assign(attrs...))
}
func (m mandantDo) Joins(fields ...field.RelationField) IMandantDo {
for _, _f := range fields {
m = *m.withDO(m.DO.Joins(_f))
}
return &m
}
func (m mandantDo) Preload(fields ...field.RelationField) IMandantDo {
for _, _f := range fields {
m = *m.withDO(m.DO.Preload(_f))
}
return &m
}
func (m mandantDo) FirstOrInit() (*model.Mandant, error) {
if result, err := m.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*model.Mandant), nil
}
}
func (m mandantDo) FirstOrCreate() (*model.Mandant, error) {
if result, err := m.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*model.Mandant), nil
}
}
func (m mandantDo) FindByPage(offset int, limit int) (result []*model.Mandant, count int64, err error) {
result, err = m.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = m.Offset(-1).Limit(-1).Count()
return
}
func (m mandantDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = m.Count()
if err != nil {
return
}
err = m.Offset(offset).Limit(limit).Scan(result)
return
}
func (m mandantDo) Scan(result interface{}) (err error) {
return m.DO.Scan(result)
}
func (m mandantDo) Delete(models ...*model.Mandant) (result gen.ResultInfo, err error) {
return m.DO.Delete(models)
}
func (m *mandantDo) withDO(do gen.Dao) *mandantDo {
m.DO = *do.(*gen.DO)
return m
}

View File

@ -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 {

View File

@ -1,19 +1,18 @@
package routers
import (
"log/slog"
"git.kocoder.xyz/kocoded/vt/model"
"git.kocoder.xyz/kocoded/vt/query"
"git.kocoder.xyz/kocoded/vt/utils"
"github.com/gofiber/fiber/v2"
)
type ansprechpartnerRouter struct {
logger *slog.Logger
utils.Application
}
func RegisterAnsprechpartnerRouter(group fiber.Router, logger *slog.Logger) {
router := &ansprechpartnerRouter{logger: logger}
func RegisterAnsprechpartnerRouter(group fiber.Router, appCtx utils.Application) {
router := &ansprechpartnerRouter{Application: appCtx}
group.Post("/new", router.createAnsprechpartner)
group.Get("/all", router.getAllAnsprechpartners)

View File

@ -1,19 +1,18 @@
package routers
import (
"log/slog"
"git.kocoder.xyz/kocoded/vt/model"
"git.kocoder.xyz/kocoded/vt/query"
"git.kocoder.xyz/kocoded/vt/utils"
"github.com/gofiber/fiber/v2"
)
type firmaRouter struct {
logger *slog.Logger
utils.Application
}
func RegisterFirmaRouter(group fiber.Router, logger *slog.Logger) {
router := &firmaRouter{logger: logger}
func RegisterFirmaRouter(group fiber.Router, appCtx utils.Application) {
router := &firmaRouter{Application: appCtx}
group.Post("/new", router.createFirma)
group.Get("/all", router.getAllFirmen)

64
routers/mandant.go Normal file
View File

@ -0,0 +1,64 @@
package routers
import (
"git.kocoder.xyz/kocoded/vt/model"
"git.kocoder.xyz/kocoded/vt/query"
"git.kocoder.xyz/kocoded/vt/utils"
"github.com/gofiber/fiber/v2"
)
type mandantRouter struct {
utils.Application
currentMandant uint
}
func RegisterMandantRouter(group fiber.Router, appCtx utils.Application) {
router := &mandantRouter{currentMandant: 1, Application: appCtx}
group.Get("/current", router.getCurrentMandant)
group.Put("/current", router.setCurrentMandant)
group.Get("/all", router.getAllMandant)
}
func (r *mandantRouter) getCurrentMandant(c *fiber.Ctx) error {
m := query.Mandant
currentMandant, err := m.Where(m.ID.Eq(r.currentMandant)).First()
if err != nil {
r.Logger.Warn("Current mandant not found.", "error", err)
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(currentMandant)
}
func (r *mandantRouter) getAllMandant(c *fiber.Ctx) error {
m := query.Mandant
mandanten, err := m.Find()
if err != nil {
r.Logger.Warn("Current mandant not found.", "error", err)
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(mandanten)
}
func (r *mandantRouter) setCurrentMandant(c *fiber.Ctx) error {
m := query.Mandant
mandant := &model.Mandant{}
if err := c.BodyParser(mandant); err != nil {
return err
}
r.currentMandant = mandant.ID
currentMandant, err := m.Where(m.ID.Eq(r.currentMandant)).First()
if err != nil {
r.Logger.Warn("Current mandant not found.", "error", err)
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(currentMandant)
}

9
types/mandant.go Normal file
View File

@ -0,0 +1,9 @@
package types
type Mandant struct {
// ID string `json:"id"`
Name string `json:"name"`
Logo string `json:"logo"`
Plan string `json:"plan"`
Color string `json:"color"`
}

20
utils/applicationCtx.go Normal file
View File

@ -0,0 +1,20 @@
package utils
import (
"log/slog"
"time"
"gorm.io/gorm"
)
type Application struct {
Logger *slog.Logger
DB *gorm.DB
ActiveSessions []Session
}
type Session struct {
Token string
UserID uint
CreatedAt time.Time
}

140
utils/authentication.go Normal file
View File

@ -0,0 +1,140 @@
package utils
import (
"context"
"encoding/json"
"net/http"
"os"
"slices"
"time"
"git.kocoder.xyz/kocoded/vt/model"
"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, appCtx Application) {
provider, err := oidc.NewProvider(ctx, "https://keycloak.kocoder.xyz/realms/che")
if err != nil {
appCtx.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 {
appCtx.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 {
appCtx.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 {
appCtx.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 {
appCtx.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 {
appCtx.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 {
appCtx.Logger.Warn("Failed to parse JSON", "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user := &model.User{}
if appCtx.DB.Where(model.User{Email: resp.UserInfo.Email}).Assign(model.User{Sub: resp.UserInfo.Subject}).FirstOrCreate(user).Error != nil {
appCtx.Logger.Warn("Failed to create user in DB")
http.Error(w, "failed to create user", http.StatusInternalServerError)
return
}
setCallbackCookieExp(w, r, "state", "", -1)
cookie, err := RandString(24)
if err != nil {
appCtx.Logger.Warn("Couldn't generate session-cookie.")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
setCallbackCookieExp(w, r, "auth-cookie", cookie, int(time.Hour.Seconds()))
appCtx.ActiveSessions = append(appCtx.ActiveSessions, Session{Token: cookie, UserID: user.ID, CreatedAt: time.Now()})
http.Redirect(w, r, "http://localhost:3001", http.StatusFound)
_, err = w.Write(data)
if err != nil {
appCtx.Logger.Error("Unable to send response", "error", err)
}
}))
app.Get("/api/auth/currentSession", func(c *fiber.Ctx) error {
authToken := c.Cookies("auth-cookie")
sessionId := slices.IndexFunc(appCtx.ActiveSessions, func(s Session) bool {
return s.Token == authToken
})
session := appCtx.ActiveSessions[sessionId]
return c.JSON(session)
})
}

49
utils/db.go Normal file
View File

@ -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.Mandant{}, &model.User{}, &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.Mandant{}, model.User{}, 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
}

36
utils/middleware.go Normal file
View File

@ -0,0 +1,36 @@
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(cors.Config{
AllowOrigins: "http://localhost:3000, http://localhost:3001",
AllowCredentials: true,
}))
// 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
}))
}

View File

@ -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
}

15
utils/random.go Normal file
View File

@ -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
}