diff --git a/.gitignore b/.gitignore index 25f6745..bc89a0b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ frontend/node_modules/ frontend/dist/ dist/ test.db-journal +logs/ diff --git a/go.mod b/go.mod index e68420f..e290050 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/spf13/viper v1.12.0 golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) require ( diff --git a/go.sum b/go.sum index ad13a03..a299eb9 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -202,7 +203,6 @@ github.com/go-pkgz/auth v1.19.0 h1:TTfbQvlqsuEhRpYAiY/uF5UGP+HWMc5CN2RAhN06ik0= github.com/go-pkgz/auth v1.19.0/go.mod h1:1pu95rx7tFfeIFq0pjQPwCeL6qGYH4+UxHJudhK0T7M= github.com/go-pkgz/repeater v1.1.3 h1:q6+JQF14ESSy28Dd7F+wRelY4F+41HJ0LEy/szNnMiE= github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw= -github.com/go-pkgz/rest v1.12.2/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk= github.com/go-pkgz/rest v1.15.6 h1:8RgOuY/c00CD0el8KdmscOCgDH+ML0ZsK2qa1Rcxal4= github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= @@ -1231,6 +1231,8 @@ gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:a gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/app/apps.go b/internal/app/apps.go index ee5ee42..45010b4 100644 --- a/internal/app/apps.go +++ b/internal/app/apps.go @@ -2,6 +2,7 @@ package app import ( "errors" + "context" "github.com/Fishwaldo/mouthpiece/internal/db" "github.com/Fishwaldo/mouthpiece/internal/errors" @@ -39,32 +40,32 @@ func InitializeApps() { db.Db.AutoMigrate(&ApplicationFilters{}) } -func GetApps() []App { +func GetApps(ctx context.Context) []App { var apps []App - db.Db.Preload("AssociatedUsers").Preload("AssociatedUsers.TransportConfigs").Preload("Filters").Find(&apps) + db.Db.WithContext(ctx).Preload("AssociatedUsers").Preload("AssociatedUsers.TransportConfigs").Preload("Filters").Find(&apps) return apps } -func FindApp(app_name string) (app *App, err error) { - tx := db.Db.Debug().Preload("AssociatedUsers").Preload("AssociatedUsers.TransportConfigs").Preload("Filters").First(&app, "app_name = ?", app_name) +func FindApp(ctx context.Context, app_name string) (app *App, err error) { + tx := db.Db.WithContext(ctx).Preload("AssociatedUsers").Preload("AssociatedUsers.TransportConfigs").Preload("Filters").First(&app, "app_name = ?", app_name) Log.V(1).Info("Finding App", "App", app_name, "Result", tx, "app", app) return app, tx.Error } -func AppExists(app_name string) bool { +func AppExists(ctx context.Context, app_name string) bool { var app App - tx := db.Db.First(&app, "app_name = ?", app_name) + tx := db.Db.WithContext(ctx).First(&app, "app_name = ?", app_name) return tx.Error == nil } -func CreateApp(app AppDetails) (newapp *App, err error) { - newapp, err = FindApp(app.AppName) +func CreateApp(ctx context.Context, app AppDetails) (newapp *App, err error) { + newapp, err = FindApp(ctx, app.AppName) if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { Log.Info("Creating New App", "App", app) var dbApp App copier.Copy(&dbApp, &app) - adminuser, _ := user.GetUser("admin@example.com") + adminuser, _ := user.GetUser(ctx, "admin@example.com") dbApp.AssociatedUsers = append(dbApp.AssociatedUsers, adminuser) - normaluser, _ := user.GetUser("user@example.com") + normaluser, _ := user.GetUser(ctx, "user@example.com") dbApp.AssociatedUsers = append(dbApp.AssociatedUsers, normaluser) if filter.FindFilter("CopyShortMessage") != nil { dbApp.Filters = append(dbApp.Filters, ApplicationFilters{Name: "CopyShortMessage"}) @@ -72,17 +73,17 @@ func CreateApp(app AppDetails) (newapp *App, err error) { if filter.FindFilter("FindSeverity") != nil { dbApp.Filters = append(dbApp.Filters, ApplicationFilters{Name: "FindSeverity"}) } - result := db.Db.Create(&dbApp) + result := db.Db.WithContext(ctx).Create(&dbApp) if result.Error != nil { return newapp, result.Error } - return FindApp(app.AppName) + return FindApp(ctx, app.AppName) } Log.Error(err, "App Already Exists", "App", newapp) return newapp, mperror.ErrAppExists } -func (app App) ProcessMessage(msg *msg.Message) error { +func (app App) ProcessMessage(ctx context.Context, msg *msg.Message) error { Log.V(1).Info("App Processing Message", "App", app.AppName, "MessageID", msg.ID) /* populate Message Fields with App Data */ msg.Body.Fields["app_description"] = app.Description @@ -92,7 +93,7 @@ func (app App) ProcessMessage(msg *msg.Message) error { flt := filter.FindFilter(appfilter.Name) if flt != nil { Log.V(1).Info("App Processing Message with Filter", "Filter", appfilter) - ok, _ := flt.ProcessMessage(msg) + ok, _ := flt.ProcessMessage(ctx, msg) if !ok { Log.Info("App Filter Blocked Message", "App", app.AppName, "Filter", appfilter, "Message", msg) return nil @@ -102,7 +103,7 @@ func (app App) ProcessMessage(msg *msg.Message) error { } } for _, user := range app.AssociatedUsers { - user.ProcessMessage(*msg) + user.ProcessMessage(ctx, *msg) } return nil } diff --git a/internal/app/restapi.go b/internal/app/restapi.go new file mode 100644 index 0000000..ec2355a --- /dev/null +++ b/internal/app/restapi.go @@ -0,0 +1,42 @@ +package app + +import ( + "net/http" + + "github.com/Fishwaldo/mouthpiece/internal/auth" + + "github.com/danielgtaylor/huma" + "github.com/danielgtaylor/huma/responses" +) + +func InitializeAppRestAPI(res *huma.Resource) error { + auth.AuthService.AddResourceURL("/v1/apps/", "apigroup:apps") + appapi := res.SubResource("/apps/") + + appapi.Get("get-apps", "Get A List of Applications", + responses.OK().ContentType("application/json"), + responses.OK().Headers("Set-Cookie"), + responses.OK().Model([]App{}), + ).Run(func(ctx huma.Context) { + ctx.WriteModel(http.StatusOK, GetApps(ctx)) + }) + + + appapi.Put("create-app", "Create a Application", + responses.OK().ContentType("application/json"), + responses.OK().Headers("Set-Cookie"), + responses.OK().Model(&App{}), + responses.NotAcceptable().ContentType("application/json"), + responses.NotAcceptable().Headers("Set-Cookie"), + ).Run(func(ctx huma.Context, input struct { + Body AppDetails + }) { + if app, err := CreateApp(ctx, input.Body); err != nil { + ctx.WriteError(http.StatusNotAcceptable, "Database Error", err) + } else { + ctx.WriteModel(http.StatusOK, app) + } + }) + + return nil +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b0a5ece..49df917 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -16,6 +16,7 @@ import ( telegramauth "github.com/Fishwaldo/mouthpiece/internal/auth/telegram" "github.com/Fishwaldo/mouthpiece/internal/db" . "github.com/Fishwaldo/mouthpiece/internal/log" + "github.com/go-logr/logr" "github.com/spf13/viper" @@ -41,14 +42,14 @@ type Auth struct { } var AuthService *Auth +var alog *authLogger +var llog logr.Logger -type AuthLogger struct { +type authLogger struct { } -var AL *AuthLogger - -func (AL AuthLogger) Logf(format string, args ...interface{}) { - Log.WithName("Auth").Info("Authentication", "message", fmt.Sprintf(format, args...)) +func (AL authLogger) Logf(format string, args ...interface{}) { + llog.V(1).Info("Authentication", "message", fmt.Sprintf(format, args...)) } type AuthConfig struct { @@ -100,7 +101,9 @@ func customGitHubProvider() (cred pkauth.Client, ch provider.CustomHandlerOpt) { } func InitAuth(Config AuthConfig) { - AL = &AuthLogger{} + llog = Log.WithName("Auth") + alog = &authLogger{} + AuthService = &Auth{} var avatarcachedir string @@ -123,14 +126,14 @@ func InitAuth(Config AuthConfig) { AvatarResizeLimit: 200, // resizes avatars to 200x200 ClaimsUpd: token.ClaimsUpdFunc(Config.MapClaimsToUser), Validator: Config.Validator, - Logger: AL, // optional logger for auth library + Logger: alog, // optional logger for auth library UseGravatar: true, // for verified provider use gravatar service } // create auth service AuthService.Service = pkauth.NewService(options) if viper.GetBool("auth.dev.enabled") { - Log.Info("Auth Dev Mode Enabled!") + llog.Info("Auth Dev Mode Enabled!") AuthService.Service.AddProvider("dev", "", "") // run dev/test oauth2 server on :8084 go func() { @@ -139,7 +142,7 @@ func InitAuth(Config AuthConfig) { return "admin@example.com" } if err != nil { - Log.Error(err, "[PANIC] failed to start dev oauth2 server") + llog.Error(err, "[PANIC] failed to start dev oauth2 server") } devAuthServer.Run(context.Background()) @@ -147,24 +150,24 @@ func InitAuth(Config AuthConfig) { } if viper.GetBool("auth.github.enabled") { if !viper.IsSet("auth.github.client_id") { - Log.Error(nil, "Github auth is enabled but client_id is not set") + llog.Error(nil, "Github auth is enabled but client_id is not set") } else { if !viper.IsSet("auth.github.client_secret") { - Log.Error(nil, "Github auth is enabled but client_secret is not set") + llog.Error(nil, "Github auth is enabled but client_secret is not set") } else { - Log.Info("Auth Github Enabled!") + llog.Info("Auth Github Enabled!") gcred, gch := customGitHubProvider() AuthService.Service.AddCustomProvider("github", gcred, gch) } } } if viper.GetBool("auth.microsoft.enabled") { - Log.Info("Auth Microsoft Enabled!") + llog.Info("Auth Microsoft Enabled!") AuthService.Service.AddProvider("microsoft", os.Getenv("AEXMPL_MS_APIKEY"), os.Getenv("AEXMPL_MS_APISEC")) } /* direct loging (username/password) is always handled */ dbprovider := dbauth.DirectHandler{ - L: AL, + L: alog, ProviderName: "direct", Issuer: options.Issuer, TokenService: AuthService.Service.TokenService(), @@ -174,7 +177,7 @@ func InitAuth(Config AuthConfig) { AuthService.Service.AddCustomHandler(dbprovider) if viper.GetBool("auth.email.enabled") { - Log.Info("Auth Email Enabled!") + llog.Info("Auth Email Enabled!") AuthService.Service.AddVerifProvider("email", "To confirm use {{.Token}}\nor follow http://arm64-1.dmz.dynam.ac:8888/auth/email/login?token={{.Token}}", provider.SenderFunc(func(address string, text string) error { // sender just prints token @@ -186,14 +189,14 @@ func InitAuth(Config AuthConfig) { if viper.GetBool("auth.telegram.enabled") { if viper.IsSet("auth.telegram.token") { - Log.Info("Auth Telegram Enabled!") + llog.Info("Auth Telegram Enabled!") // add telegram provider telegram := telegramauth.TelegramHandler{ ProviderName: "telegram", ErrorMsg: "❌ Invalid auth request. Please try clicking link again.", SuccessMsg: "✅ You have successfully authenticated!", Telegram: telegramauth.NewTelegramAPI(viper.GetString("auth.telegram.token"), http.DefaultClient), - L: AL, + L: alog, TokenService: AuthService.Service.TokenService(), AvatarSaver: AuthService.Service.AvatarProxy(), } @@ -201,49 +204,49 @@ func InitAuth(Config AuthConfig) { go func() { err := telegram.Run(context.Background()) if err != nil { - Log.Error(err, "[PANIC] failed to start telegram") + llog.Error(err, "[PANIC] failed to start telegram") } }() AuthService.Service.AddCustomHandler(&telegram) } else { - Log.Error(nil, "Telegram auth is enabled but token is not set") + llog.Error(nil, "Telegram auth is enabled but token is not set") } } InitCasbin(Config) - Log.Info("Auth service started") + llog.Info("Auth service started") } func InitCasbin(config AuthConfig) { cdb, err := gormadapter.NewAdapterByDB(db.Db) if err != nil { - Log.Error(err, "Failed to Setup Casbin Auth Adapter") + llog.Error(err, "Failed to Setup Casbin Auth Adapter") } casbinmodel, err := fs.ReadFile(config.ConfigDir, "config/auth_model.conf") if err != nil { - Log.Error(err, "Failed to read casbin model") + llog.Error(err, "Failed to read casbin model") } m, err := model.NewModelFromString(string(casbinmodel)) if err != nil { - Log.Error(err, "Failed to parse casbin model") + llog.Error(err, "Failed to parse casbin model") } AuthService.AuthEnforcer, err = casbin.NewEnforcer(m, cdb) if err != nil { - Log.Error(err, "Failed to setup Casbin") + llog.Error(err, "Failed to setup Casbin") } AuthService.AuthEnforcer.EnableLog(viper.GetBool("auth.debug")) AuthService.AuthEnforcer.EnableAutoSave(true) AuthService.AuthEnforcer.SetRoleManager(defaultrolemanager.NewRoleManager(10)) //if err := AuthService.AuthEnforcer.LoadModel(); err != nil { - // Log.Error(err, "Failed to load Casbin model") + // llog.Error(err, "Failed to load Casbin model") //} if err := AuthService.AuthEnforcer.LoadPolicy(); err != nil { - Log.Error(err, "Failed to Load Casbin Policy") + llog.Error(err, "Failed to Load Casbin Policy") } if !AuthService.AuthEnforcer.AddNamedMatchingFunc("g2", "KeyMatch3", util.KeyMatch3) { - Log.Error(nil, "Failed to add g2 matching function") + llog.Error(nil, "Failed to add g2 matching function") } AuthService.AuthEnforcer.AddPolicy("role:admin", "apigroup:apps", "PUT") AuthService.AuthEnforcer.AddPolicy("role:user", "apigroup:apps", "GET") @@ -259,21 +262,20 @@ func InitCasbin(config AuthConfig) { // AuthService.AuthEnforcer.AddRoleForUser("admin", "role:admin") // AuthService.AuthEnforcer.AddRoleForUser("dev_user", "role:admin") - p, _ := AuthService.AuthEnforcer.GetImplicitPermissionsForUser("admin@example.com") - fmt.Printf("Admin Permissions: %+v\n", p) + AuthService.AuthEnforcer.SavePolicy() rm := AuthService.AuthEnforcer.GetPolicy() - Log.Info("Casbin Policy", "policy", rm) - Log.Info("Casbin User Roles", "Roles", AuthService.AuthEnforcer.GetGroupingPolicy()) - Log.Info("Casbin API Groups", "API Groups", AuthService.AuthEnforcer.GetNamedGroupingPolicy("g2")) + llog.Info("Casbin Policy", "policy", rm) + llog.Info("Casbin User Roles", "Roles", AuthService.AuthEnforcer.GetGroupingPolicy()) + llog.Info("Casbin API Groups", "API Groups", AuthService.AuthEnforcer.GetNamedGroupingPolicy("g2")) } func (a *Auth) AddResourceURL(url string, group string) bool { ok, err := a.AuthEnforcer.AddNamedGroupingPolicy("g2", url, group) if err != nil { - Log.Error(err, "Failed to add g2 policy", "url", url, "group", group) + llog.Error(err, "Failed to add g2 policy", "url", url, "group", group) } return ok } diff --git a/internal/filter/filter.go b/internal/filter/filter.go index 840d5db..db0fad6 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -4,15 +4,16 @@ import ( "io/fs" "path/filepath" "strings" + "context" //"io/ioutil" "embed" "fmt" "github.com/Fishwaldo/mouthpiece/internal/db" - . "github.com/Fishwaldo/mouthpiece/internal/log" + "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/message" - + "github.com/go-logr/logr" "github.com/skx/evalfilter/v2" "github.com/skx/evalfilter/v2/object" "gorm.io/gorm" @@ -23,6 +24,9 @@ var ScriptFiles embed.FS type FilterType int +var llog logr.Logger + + const ( AppFilter = iota UserFilter @@ -71,10 +75,10 @@ func loadScriptFiles(files []string, scripttype FilterType) { var flt *Filter tx := db.Db.Where("name = ? and type = ?", trimFileExtension(filepath.Base(script)), scripttype).First(&flt) if tx.RowsAffected == 0 { - Log.Info("Reading Filter Script from Filesystem", "type", scripttype, "filter", trimFileExtension(filepath.Base(script))) + llog.V(1).Info("Reading Filter Script from Filesystem", "type", scripttype, "filter", trimFileExtension(filepath.Base(script))) content, err := fs.ReadFile(ScriptFiles, script) if err != nil { - Log.Error(err, "Failed to read Filter Script File", "filename", script) + llog.Error(err, "Failed to read Filter Script File", "filename", script) continue } // @@ -86,19 +90,20 @@ func loadScriptFiles(files []string, scripttype FilterType) { Type: scripttype, } } else { - Log.Info("Loading Filter Script from Databse", "type", scripttype, "filter", flt.Name) + llog.V(1).Info("Loading Filter Script from Databse", "type", scripttype, "filter", flt.Name) } if err := flt.SetupEvalFilter(); err == nil { - Log.Info("Loaded Filter Script ", "type", scripttype, "filter", flt.Name) + llog.Info("Loaded Filter Script ", "type", scripttype, "filter", flt.Name) Filters = append(Filters, flt) db.Db.Save(flt) } else { - Log.Error(err, "Failed to load Filter Script", "type", scripttype, "filter", flt.Name) + llog.Error(err, "Failed to load Filter Script", "type", scripttype, "filter", flt.Name) } } } func InitFilter() { + llog = log.Log.WithName("filter") db.Db.AutoMigrate(&Filter{}) Filters = make([]*Filter, 0) scripts := filterFiles("scripts/apps", ".scp") @@ -143,13 +148,13 @@ func (ev *Filter) fnPrintf(args []object.Object) object.Object { // Call the helper out := fmt.Sprintf(fs, fmtArgs...) - Log.Info("Filter Script Output", "filter", ev.Name, "output", out) + llog.Info("Filter Script Output", "filter", ev.Name, "output", out) return &object.Void{} } func (ev *Filter) fnPrint(args []object.Object) object.Object { for _, e := range args { - Log.Info("Filter Script Output", "filter", ev.Name, "Output", e.Inspect()) + llog.Info("Filter Script Output", "filter", ev.Name, "Output", e.Inspect()) } return &object.Void{} } @@ -170,7 +175,7 @@ func (ev *Filter) fnSetField(args []object.Object) object.Object { arg := args[0].ToInterface() - Log.Info("Setting Field Value", "filter", ev.Name, "field", fld, "value", arg) + llog.Info("Setting Field Value", "filter", ev.Name, "field", fld, "value", arg) ev.processedMessage.Body.Fields[fld] = arg return &object.Void{} } @@ -184,11 +189,11 @@ func (ev *Filter) fnClearField(args []object.Object) object.Object { return &object.Null{} } fld := args[0].(*object.String).Value - Log.Info("Clearing Field Value", "filter", ev.Name, "field", fld) + llog.Info("Clearing Field Value", "filter", ev.Name, "field", fld) if _, ok := ev.processedMessage.Body.Fields[fld]; ok { delete(ev.processedMessage.Body.Fields, fld) } else { - Log.Info("Field Not Found", "filter", ev.Name, "field", fld) + llog.Info("Field Not Found", "filter", ev.Name, "field", fld) } return &object.Void{} } @@ -202,7 +207,7 @@ func (ev *Filter) fnSetShortMessage(arg []object.Object) object.Object { return &object.Null{} } msg := arg[0].(*object.String).Value - Log.Info("Setting Short Message", "filter", ev.Name, "message", msg) + llog.Info("Setting Short Message", "filter", ev.Name, "message", msg) ev.processedMessage.Body.ShortMsg = msg return &object.Void{} } @@ -216,32 +221,33 @@ func (ev *Filter) fnSetSeverity(arg []object.Object) object.Object { return &object.Null{} } msg := arg[0].(*object.String).Value - Log.Info("Setting Severity", "filter", ev.Name, "Severity", msg) + llog.Info("Setting Severity", "filter", ev.Name, "Severity", msg) ev.processedMessage.Body.Severity = msg return &object.Void{} } -func (ev *Filter) ProcessMessage(msg *msg.Message) (bool, error) { +func (ev *Filter) ProcessMessage(ctx context.Context, msg *msg.Message) (bool, error) { if !ev.Enabled { return true, nil } defer func() { if err := recover(); err != nil { - Log.Error(err.(error), "Filter Script Error", "filter", ev.Name) + llog.Error(err.(error), "Filter Script Error", "filter", ev.Name) } }() if !ev.ok { - Log.Info("Filter Script Not ready", "filter", ev.Name) + llog.Info("Filter Script Not ready", "filter", ev.Name) return true, nil } ev.processedMessage = msg + ev.script.SetContext(ctx) ok, err := ev.script.Run(msg.Body) ev.processedMessage = nil if err != nil { - Log.Info("Filter Run Failed", "filter", ev.Name, "result", ok, "Error", err) + llog.Info("Filter Run Failed", "filter", ev.Name, "result", ok, "Error", err) return true, err } - Log.V(1).Info("Filter Run Success", "filter", ev.Name, "result", ok) + llog.V(1).Info("Filter Run Success", "filter", ev.Name, "result", ok) return ok, nil } @@ -250,7 +256,7 @@ func (ev *Filter) SetupEvalFilter() error { // Create an evaluator, with the script inside it. // ev.script = evalfilter.New(ev.Content) - Log.Info("Filter Script Content", "filter", ev.Name, "content", ev.Content) + llog.V(1).Info("Filter Script Content", "filter", ev.Name, "content", ev.Content) ev.script.AddFunction("printf", ev.fnPrintf) ev.script.AddFunction("print", ev.fnPrint) ev.script.AddFunction("setfield", ev.fnSetField) @@ -258,11 +264,11 @@ func (ev *Filter) SetupEvalFilter() error { ev.script.AddFunction("setshortmessage", ev.fnSetShortMessage) ev.script.AddFunction("setseverity", ev.fnSetSeverity) if err := ev.script.Prepare(); err != nil { - Log.Info("Compile Filter Script Failed", "filter", ev.Name, "error", err) + llog.Error(err, "Compile Filter Script Failed", "filter", ev.Name) ev.ok = false return err } - Log.Info("Compile Filter Script Success", "filter", ev.Name) + llog.V(1).Info("Compile Filter Script Success", "filter", ev.Name) ev.ok = true return nil } diff --git a/internal/health/health.go b/internal/health/health.go index 6d9f548..17a9a52 100644 --- a/internal/health/health.go +++ b/internal/health/health.go @@ -6,17 +6,17 @@ import ( "time" "github.com/Fishwaldo/mouthpiece/internal/db" - . "github.com/Fishwaldo/mouthpiece/internal/log" - + "github.com/Fishwaldo/mouthpiece/internal/log" + "github.com/go-logr/logr" "github.com/alexliesenfeld/health" httpCheck "github.com/hellofresh/health-go/v4/checks/http" ) - +var llog logr.Logger var HealthChecker health.Checker func StartHealth() { - + llog = log.Log.WithName("health") HealthChecker = health.NewChecker( health.WithTimeout(10*time.Second), //health.WithInterceptors(interceptors.BasicLogger()), @@ -48,7 +48,7 @@ func BasicLogger() health.Interceptor { return func(ctx context.Context, name string, state health.CheckState) health.CheckState { now := time.Now() result := next(ctx, name, state) - Log.V(1).Info("processed health check request", + llog.V(1).Info("processed health check request", "check", name, "seconds", time.Now().Sub(now).Seconds(), "result", result.Status) return result } diff --git a/internal/log/log.go b/internal/log/log.go index 319264c..89b4e32 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -2,21 +2,81 @@ package log import ( "fmt" - "github.com/danielgtaylor/huma/middleware" + "time" + "path/filepath" + "github.com/go-logr/logr" "github.com/go-logr/zapr" + "github.com/spf13/viper" "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" + ) var Log logr.Logger var zapLog *zap.Logger +func init() { + viper.SetDefault("debug", false) + viper.SetDefault("log.dir", "logs") + viper.SetDefault("log.maxsize", 1) + viper.SetDefault("log.maxbackups", 3) + viper.SetDefault("log.maxage", 7) + viper.SetDefault("log.compress", true) + viper.SetDefault("log.level", "info") + +} + func InitLogger() { - zapLog, err := middleware.NewDefaultLogger() - if err != nil { - panic(fmt.Sprintf("Initilize Logging Failed (%v)?", err)) + var cfg zap.Config + var lvl zapcore.Level + var err error + if lvl, err = zapcore.ParseLevel(viper.GetString("log.level")); err != nil { + panic(err) + } + if viper.GetBool("debug") { + fmt.Printf("Debug Enabled at %s level\n", viper.GetString("log.level")) + cfg = zap.NewDevelopmentConfig() + cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + cfg.OutputPaths = []string{"stdout"} + cfg.EncoderConfig.EncodeTime = iso8601UTCTimeEncoder + cfg.Level = zap.NewAtomicLevelAt(lvl) + var err error + if zapLog, err = cfg.Build(); err != nil { + panic(err) + } + } else { + fmt.Printf("Debug Disabled. Logging to file %s at %s level\n", filepath.Join(viper.GetString("log.dir"), "mouthpiece.log"), viper.GetString("log.level")) + lumberJackLogger := &lumberjack.Logger{ + Filename: filepath.Join(viper.GetString("log.dir"), "mouthpiece.log"), + MaxSize: viper.GetInt("log.maxsize"), // megabytes + MaxBackups: viper.GetInt("log.maxbackups"), + MaxAge: viper.GetInt("log.maxage"), //days + Compress: viper.GetBool("log.compress"), + } + ws := zapcore.AddSync(lumberJackLogger) + enccfg := zap.NewProductionEncoderConfig() + enccfg.EncodeTime = iso8601UTCTimeEncoder + core := zapcore.NewCore( + zapcore.NewJSONEncoder(enccfg), + ws, + zap.NewAtomicLevelAt(lvl), + ) + + zapLog = zap.New(core) + zap.ReplaceGlobals(zapLog) } Log = zapr.NewLogger(zapLog) - - Log.Info("Logging Started") + zap.RedirectStdLog(zapLog) + Log.Info("Logging Started", "level", viper.GetString("log.level")) } + +// A UTC variation of ZapCore.ISO8601TimeEncoder with millisecond precision +func iso8601UTCTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.UTC().Format("2006-01-02T15:04:05.000Z")) +} + +func GetZapLogger() (*zap.Logger, error) { + return zapLog.Named("huma"), nil +} \ No newline at end of file diff --git a/internal/middleware/accesscontrol.go b/internal/middleware/accesscontrol.go index effeffb..4c810e0 100644 --- a/internal/middleware/accesscontrol.go +++ b/internal/middleware/accesscontrol.go @@ -2,7 +2,7 @@ package middleware import ( "context" - // "fmt" + "strconv" "net/http" "github.com/Fishwaldo/mouthpiece/internal/auth" @@ -17,7 +17,7 @@ import ( type Middleware struct { } -type CtxUserValue struct{} + // Update user info in request context from go-pkgz/auth token.User to mouthpiece.User func (a *Middleware) Update() func(http.Handler) http.Handler { @@ -26,11 +26,13 @@ func (a *Middleware) Update() func(http.Handler) http.Handler { // call update only if user info exists, otherwise do nothing if tknuser, err := token.GetUserInfo(r); err == nil { /* find out DB User */ - if dbUser, err := user.GetUser(tknuser.Email); err != nil { + id, _ := strconv.Atoi(tknuser.ID) + ctx := huma.ContextFromRequest(w, r) + if dbUser, err := user.GetUserByID(ctx, uint(id)); err != nil { Log.Info("DBUser Not Found", "token", tknuser, "error", err) - ctx := huma.ContextFromRequest(w, r) + /* do Something */ - ctx.WriteError(http.StatusForbidden, "User not found", err) + ctx.WriteError(http.StatusUnauthorized, "User not found", err) return } else { ok, res, err := auth.AuthService.AuthEnforcer.EnforceEx(dbUser.Email, r.URL.Path, r.Method) @@ -39,7 +41,7 @@ func (a *Middleware) Update() func(http.Handler) http.Handler { huma.ContextFromRequest(w, r).WriteError(http.StatusForbidden, "Access Denied", err) return } - r = r.WithContext(context.WithValue(r.Context(), CtxUserValue{}, tknuser)) + r = r.WithContext(context.WithValue(r.Context(), user.CtxUserValue{}, tknuser)) } h.ServeHTTP(w, r) return @@ -52,3 +54,4 @@ func (a *Middleware) Update() func(http.Handler) http.Handler { } return f } + diff --git a/internal/router.go b/internal/router.go index f249c70..df4ed69 100644 --- a/internal/router.go +++ b/internal/router.go @@ -1,15 +1,17 @@ package mouthpiece import ( + "context" + "github.com/Fishwaldo/mouthpiece/internal/app" "github.com/Fishwaldo/mouthpiece/internal/errors" . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/message" ) -func RouteMessage(msg *msg.Message) { - if app, err := app.FindApp(msg.AppName); err == nil { - app.ProcessMessage(msg) +func RouteMessage(ctx context.Context, msg *msg.Message) { + if app, err := app.FindApp(ctx, msg.AppName); err == nil { + app.ProcessMessage(ctx, msg) } else { Log.Error(mperror.ErrAppNotFound, "App Not Found", "App", msg.AppName) } diff --git a/internal/transport/stdout/stdout.go b/internal/transport/stdout/stdout.go index f4dede3..4e33b41 100644 --- a/internal/transport/stdout/stdout.go +++ b/internal/transport/stdout/stdout.go @@ -2,6 +2,7 @@ package stdout import ( "fmt" + "context" . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/message" "github.com/Fishwaldo/mouthpiece/internal/transport" @@ -23,11 +24,11 @@ func (t StdoutTransport) GetName() string { return "stdout" } -func (t StdoutTransport) SendMessage(config transport.TransportConfig, msg msg.Message) (err error) { +func (t StdoutTransport) SendMessage(ctx context.Context, config transport.TransportConfig, msg msg.Message) (err error) { fmt.Println("=========================================================") fmt.Printf("Message: %s\n", msg.Body.Message) fmt.Println("=========================================================") - transport.UpdateTransportStatus(t, msg, "sent") + transport.UpdateTransportStatus(ctx, t, msg, "sent") return nil } @@ -35,7 +36,7 @@ func (t StdoutTransport) Start() { Log.Info("Transport Started", "name", t.GetName()) } -func (t StdoutTransport) NewTransportConfig() { +func (t StdoutTransport) NewTransportConfig(ctx context.Context) { // user.TransportConfigs = append(user.TransportConfigs, mouthpiece.TransportConfig{ // Transport: t.GetName(), // Config: user.Username, diff --git a/internal/transport/telegram/telegram.go b/internal/transport/telegram/telegram.go index 3433c73..b1d48fc 100644 --- a/internal/transport/telegram/telegram.go +++ b/internal/transport/telegram/telegram.go @@ -2,7 +2,7 @@ package telegram import ( "fmt" - // "os" + "context" . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/message" @@ -73,17 +73,17 @@ func (t TelegramTransport) Start() { Log.Info("Transport Started", "name", t.GetName()) } -func (t TelegramTransport) NewTransportConfig() { +func (t TelegramTransport) NewTransportConfig(ctx context.Context) { // user.TransportConfigs = append(user.TransportConfigs, mouthpiece.TransportConfig{ // Transport: t.GetName(), // Config: user.Username, // }) } -func (t TelegramTransport) SendMessage(config transport.TransportConfig, msg msg.Message) (err error) { +func (t TelegramTransport) SendMessage(ctx context.Context, config transport.TransportConfig, msg msg.Message) (err error) { fmt.Println("=========================================================") fmt.Printf("Message: %s\n", msg.Body.Message) fmt.Println("=========================================================") - transport.UpdateTransportStatus(t, msg, "sent") + transport.UpdateTransportStatus(ctx, t, msg, "sent") return nil } diff --git a/internal/transport/transports.go b/internal/transport/transports.go index fa0f404..7beb064 100644 --- a/internal/transport/transports.go +++ b/internal/transport/transports.go @@ -2,6 +2,7 @@ package transport import ( "errors" + "context" "github.com/Fishwaldo/mouthpiece/internal/db" . "github.com/Fishwaldo/mouthpiece/internal/log" @@ -19,8 +20,8 @@ type TransportConfig struct { type ITransport interface { GetName() string Start() - SendMessage(config TransportConfig, message msg.Message) (err error) - NewTransportConfig() + SendMessage(ctx context.Context, config TransportConfig, message msg.Message) (err error) + NewTransportConfig(ctx context.Context) } var transports map[string]ITransport @@ -43,14 +44,14 @@ func StartTransports() { } } -func GetTransport(name string) (ITransport, error) { +func GetTransport(ctx context.Context, name string) (ITransport, error) { if t, ok := transports[name]; ok { return t, nil } return nil, errors.New("Transport Not Found") } -func GetTransports() []string { +func GetTransports(ctx context.Context) []string { var a []string for k := range transports { a = append(a, k) @@ -58,6 +59,6 @@ func GetTransports() []string { return a } -func UpdateTransportStatus(t ITransport, m msg.Message, status string) { +func UpdateTransportStatus(ctx context.Context, t ITransport, m msg.Message, status string) { Log.Info("Transport Status", "status", status, "MessageID", m.ID, "Transport", t.GetName()) } diff --git a/internal/user/auth.go b/internal/user/auth.go index 6bd82e6..1c9dba1 100644 --- a/internal/user/auth.go +++ b/internal/user/auth.go @@ -1,6 +1,9 @@ package user import ( + "context" + "fmt" + "strconv" "strings" "github.com/Fishwaldo/mouthpiece/internal/errors" @@ -8,17 +11,20 @@ import ( "github.com/go-pkgz/auth/token" ) +type CtxUserValue struct{} + + func dbAuthProvider(user, pass string) (ok bool, err error) { user = strings.TrimSpace(user) - Log.Info("Direct Login", "user", user, "pass", pass) - dbUser, err := GetUser(user) + Log.Info("Direct Login", "user", user) + dbUser, err := GetUser(context.Background(), user) Log.Info("User", "user", dbUser, "error", err) if err == mperror.ErrUserNotFound { Log.Info("User not found", "user", user) return false, nil } - if !dbUser.CheckPassword(pass) { + if !dbUser.CheckPassword(context.Background(), pass) { Log.Info("Password Invalid", "user", user) return false, nil } @@ -27,7 +33,7 @@ func dbAuthProvider(user, pass string) (ok bool, err error) { // Called when the Tokens are created/refreshed. func MapClaimsToUser(claims token.Claims) token.Claims { - Log.Info("Map Claims To User", "claims", claims) + //Log.Info("Map Claims To User", "claims", claims) // if claims.User != nil { // if user, err := GetUser(claims.User.Name); err != nil { // Log.Info("User not found", "user", claims.User.Name) @@ -42,9 +48,22 @@ func MapClaimsToUser(claims token.Claims) token.Claims { // called on every access to the API func UserValidator(token string, claims token.Claims) bool { - Log.Info("User Validator", "token", token, "claims", claims) + //Log.Info("User Validator", "user", claims.User.Name) if claims.User != nil { - return true - } + if user, _ := GetUser(context.Background(), claims.User.Name); user != nil { + claims.User.ID = fmt.Sprintf("%d", user.ID) + return true + } + } return false } + +func GetUserFromContext(ctx context.Context) (bool, *User) { + v := ctx.Value(CtxUserValue{}).(token.User) + if id, _ := strconv.Atoi(v.ID); id > 0 { + if user, _ := GetUserByID(ctx, uint(id)); user != nil { + return true, user + } + } + return false, nil +} \ No newline at end of file diff --git a/internal/user/users.go b/internal/user/users.go index 819d214..e88600b 100644 --- a/internal/user/users.go +++ b/internal/user/users.go @@ -1,14 +1,16 @@ package user import ( + "context" "fmt" + "golang.org/x/crypto/bcrypt" "github.com/Fishwaldo/mouthpiece/internal/auth" "github.com/Fishwaldo/mouthpiece/internal/db" - "github.com/Fishwaldo/mouthpiece/internal/errors" + mperror "github.com/Fishwaldo/mouthpiece/internal/errors" . "github.com/Fishwaldo/mouthpiece/internal/log" - "github.com/Fishwaldo/mouthpiece/internal/message" + msg "github.com/Fishwaldo/mouthpiece/internal/message" "github.com/Fishwaldo/mouthpiece/internal/transport" "github.com/go-playground/validator/v10" @@ -36,27 +38,27 @@ func init() { } } -func CreateUser(user *User) error { +func CreateUser(ctx context.Context, user *User) error { validate := validator.New() if err := validate.Struct(user); err != nil { Log.Info("User Validation Error", "Error", err) return err } - tx := db.Db.Omit("Password").Create(&user) + tx := db.Db.WithContext(ctx).Omit("Password").Create(&user) if tx.Error != nil { return tx.Error } - if dbuser, err := GetUser(user.Email); err == nil { + if dbuser, err := GetUser(ctx, user.Email); err == nil { /* Set the Users Initial Password */ - if err := dbuser.SetPassword(user.Password); err != nil { - if tx := db.Db.Delete(&dbuser); tx.Error != nil { + if err := dbuser.SetPassword(ctx, user.Password); err != nil { + if tx := db.Db.WithContext(ctx).Delete(&dbuser); tx.Error != nil { Log.Info("Error Deleting User after failed Password", "Error", tx.Error) return err } return err } /* New Users all Start with User Role */ - if !dbuser.addUserRole("user") { + if !dbuser.addUserRole(ctx, "user") { Log.Info("Error Adding User Role", "Error", err) } return nil @@ -65,7 +67,7 @@ func CreateUser(user *User) error { } } -func (u *User) addUserRole(role string) bool { +func (u *User) addUserRole(ctx context.Context, role string) bool { _, err := auth.AuthService.AuthEnforcer.AddRoleForUser(u.Email, fmt.Sprintf("role:%s", role)) if err != nil { Log.Info("Failed to add role for user", "email", u.Email, "role", role, "error", err) @@ -74,7 +76,7 @@ func (u *User) addUserRole(role string) bool { return true } -func (u *User) CheckPassword(password string) bool { +func (u *User) CheckPassword(ctx context.Context, password string) bool { Log.Info("Checking Password", "email", u.Email) err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) if err != nil { @@ -84,14 +86,14 @@ func (u *User) CheckPassword(password string) bool { return true } -func (u *User) SetPassword(password string) error { +func (u *User) SetPassword(ctx context.Context, password string) error { Log.Info("Setting Password", "Email", u.Email) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { Log.Info("Error Generating SetPassword Hash", "Error", err) return err } - if tx := db.Db.Model(&u).Update("password", string(hashedPassword)); tx.Error != nil { + if tx := db.Db.WithContext(ctx).Model(&u).Update("password", string(hashedPassword)); tx.Error != nil { Log.Info("Error Setting Password", "Error", tx.Error) return tx.Error } @@ -106,49 +108,49 @@ func InitializeUsers() { if count == 0 { Log.Info("Creating Default Users") admin := &User{FirstName: "Admin", LastName: "User", Email: "admin@example.com", Password: "password"} - if err := CreateUser(admin); err == nil { - admin.addUserRole("admin") + if err := CreateUser(context.Background(), admin); err == nil { + admin.addUserRole(context.Background(), "admin") Log.Info("Created Default Admin admin@example.com") } - if err := CreateUser(&User{FirstName: "User", LastName: "User", Email: "user@example.com", Password: "password"}); err == nil { + if err := CreateUser(context.Background(), &User{FirstName: "User", LastName: "User", Email: "user@example.com", Password: "password"}); err == nil { Log.Info("Created Default User user@example.com") } } } -func GetUsers() []User { +func GetUsers(ctx context.Context) []User { var users []User - db.Db.Find(&users) + db.Db.WithContext(ctx).Find(&users) return users } -func GetUser(email string) (user *User, err error) { - tx := db.Db.Preload(clause.Associations).First(&user, "email = ?", email) +func GetUser(ctx context.Context, email string) (user *User, err error) { + tx := db.Db.WithContext(ctx).Preload(clause.Associations).First(&user, "email = ?", email) if tx.Error == gorm.ErrRecordNotFound { return nil, mperror.ErrUserNotFound } return } -func GetUserByID(id uint) (user *User, err error) { - tx := db.Db.Preload(clause.Associations).First(&user, "ID = ?", id) +func GetUserByID(ctx context.Context, id uint) (user *User, err error) { + tx := db.Db.WithContext(ctx).Preload(clause.Associations).First(&user, "ID = ?", id) if tx.Error == gorm.ErrRecordNotFound { return nil, mperror.ErrUserNotFound } return } -func (u User) ProcessMessage(msg msg.Message) (err error) { +func (u User) ProcessMessage(ctx context.Context, msg msg.Message) (err error) { /* add User Fields to Message */ msg.Body.Fields["first_name"] = u.FirstName msg.Body.Fields["last_name"] = u.LastName msg.Body.Fields["email"] = u.Email Log.V(1).Info("User Processing Message", "Email", u.Email, "MessageID", msg.ID) for _, tc := range u.TransportConfigs { - t, err := transport.GetTransport(tc.Transport) + t, err := transport.GetTransport(ctx, tc.Transport) if err != nil { Log.Info("Cant find Transport", "Transport", tc.Transport) } - go t.SendMessage(tc, msg) + go t.SendMessage(ctx, tc, msg) } return } diff --git a/main.go b/main.go index 227eddc..475165b 100644 --- a/main.go +++ b/main.go @@ -25,28 +25,25 @@ SOFTWARE. package main import ( - //"fmt" - // "context" + "embed" "fmt" "io/fs" "net/http" - - // "reflect" - "strings" - // "unsafe" - "embed" - "encoding/json" "os" - "runtime/debug" + "os/signal" + "strings" + "syscall" + "time" + "context" + "path/filepath" "github.com/Fishwaldo/mouthpiece/frontend" - _ "github.com/Fishwaldo/mouthpiece/frontend" mouthpiece "github.com/Fishwaldo/mouthpiece/internal" "github.com/Fishwaldo/mouthpiece/internal/app" "github.com/Fishwaldo/mouthpiece/internal/auth" "github.com/Fishwaldo/mouthpiece/internal/db" "github.com/Fishwaldo/mouthpiece/internal/filter" - . "github.com/Fishwaldo/mouthpiece/internal/log" + "github.com/Fishwaldo/mouthpiece/internal/log" msg "github.com/Fishwaldo/mouthpiece/internal/message" "github.com/Fishwaldo/mouthpiece/internal/middleware" "github.com/Fishwaldo/mouthpiece/internal/transport" @@ -61,14 +58,50 @@ import ( "github.com/go-chi/chi" "github.com/danielgtaylor/huma" - "github.com/danielgtaylor/huma/cli" + hmw "github.com/danielgtaylor/huma/middleware" "github.com/danielgtaylor/huma/responses" + "github.com/spf13/cobra" "github.com/spf13/viper" ) //go:embed config var ConfigFiles embed.FS +type mpserver struct { + *huma.Router + root *cobra.Command + prestart []func() +} + +var Server mpserver + +func (c *mpserver) Flag(name, short, description string, defaultValue interface{}) { + viper.SetDefault(name, defaultValue) + + flags := c.root.PersistentFlags() + switch v := defaultValue.(type) { + case bool: + flags.BoolP(name, short, viper.GetBool(name), description) + case int, int16, int32, int64, uint16, uint32, uint64: + flags.IntP(name, short, viper.GetInt(name), description) + case float32, float64: + flags.Float64P(name, short, viper.GetFloat64(name), description) + default: + flags.StringP(name, short, fmt.Sprintf("%v", v), description) + } + viper.BindPFlag(name, flags.Lookup(name)) +} + +func (c *mpserver) PreStart(f func()) { + c.prestart = append(c.prestart, f) +} + +func (c *mpserver) Run() { + if err := c.root.Execute(); err != nil { + panic(err) + } +} + func init() { viper.SetDefault("frontend.path", "frontend/dist") viper.SetDefault("frontend.external", false) @@ -81,7 +114,7 @@ func fileServer(r chi.Router, path string, root http.FileSystem) { panic("FileServer does not permit URL parameters.") } - //log.Printf("[INFO] serving static files from %v", root) + //log.Log.Printf("[INFO] serving static files from %v", root) fs := http.StripPrefix(path, http.FileServer(root)) if path != "/" && path[len(path)-1] != '/' { @@ -95,21 +128,12 @@ func fileServer(r chi.Router, path string, root http.FileSystem) { }) } -func printBuildInfo() { - bi, ok := debug.ReadBuildInfo() - if !ok { - fmt.Println("Getting build info failed (not in module mode?)!") - return - } - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - if err := enc.Encode(bi); err != nil { - panic(err) - } -} func main() { + viper.SetEnvPrefix("MP") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") @@ -139,21 +163,51 @@ func main() { fmt.Println(bi.String()) // Create a new router & CLI with default middleware. - InitLogger() + + Server = mpserver{ + Router: huma.New(bi.Name, bi.GitVersion), + } + hmw.Defaults(Server.Router) + Server.root = &cobra.Command{ + Use: filepath.Base(os.Args[0]), + Version: bi.GitVersion, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Starting %s (%s)\n", bi.Name, bi.GitVersion) + for _, f := range Server.prestart { + f() + } + go func() { + if err := Server.Listen(fmt.Sprintf("%s:%v", viper.Get("host"), viper.Get("port"))); err != nil && err != http.ErrServerClosed { + panic(err) + } + }() + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + fmt.Println("Shutting down...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + Server.Shutdown(ctx) + }, + } + Server.Flag("host", "", "Hostname", "0.0.0.0") + Server.Flag("port", "p", "Port", 8888) + + + log.InitLogger() db.InitializeDB() - humucli := cli.NewRouter(bi.Name, bi.GitVersion) - humucli.DisableSchemaProperty() - humucli.PreStart(transport.InitializeTransports) - humucli.PreStart(msg.InitializeMessage) - humucli.PreStart(user.InitializeUsers) - humucli.PreStart(app.InitializeApps) - humucli.PreStart(transport.StartTransports) - humucli.PreStart(filter.InitFilter) - // app.PreStart() - humucli.PreStart(healthChecker.StartHealth) - humucli.GatewayClientCredentials("mouthpiece", "/oauth2/token", nil) - humucli.GatewayAuthCode("mouthpiece2", "/oauth2/token", "/oauth2/token", nil) - humucli.GatewayBasicAuth("basic") + hmw.NewLogger = log.GetZapLogger + Server.DisableSchemaProperty() + Server.PreStart(transport.InitializeTransports) + Server.PreStart(msg.InitializeMessage) + Server.PreStart(user.InitializeUsers) + Server.PreStart(app.InitializeApps) + Server.PreStart(transport.StartTransports) + Server.PreStart(filter.InitFilter) + Server.PreStart(healthChecker.StartHealth) + Server.GatewayClientCredentials("mouthpiece", "/oauth2/token", nil) + Server.GatewayAuthCode("mouthpiece2", "/oauth2/token", "/oauth2/token", nil) + Server.GatewayBasicAuth("basic") user.AuthConfig.Host = fmt.Sprintf("http://arm64-1.dmz.dynam.ac:%v", viper.Get("Port")) user.AuthConfig.ConfigDir = ConfigFiles @@ -162,26 +216,26 @@ func main() { p := middleware.Middleware{} authRoutes, avaRoutes := auth.AuthService.Service.Handlers() - mux := humucli.Resource("/").GetMux() + mux := Server.Resource("/").GetMux() mux.Mount("/auth", authRoutes) mux.Mount("/avatar", avaRoutes) var httpfiles http.FileSystem if viper.GetBool("frontend.external") { - Log.Info("Serving frontend from external location", "path", viper.GetString("frontend.path")) + log.Log.Info("Serving frontend from external location", "path", viper.GetString("frontend.path")) httpfiles = http.Dir(viper.GetString("frontend.path")) } else { - Log.Info("Serving frontend from Bundled Files") + log.Log.Info("Serving frontend from Bundled Files") subdir, err := fs.Sub(frontend.FrontEndFiles, "dist") if err != nil { - Log.Error(err, "Failed to get subdir") + log.Log.Error(err, "Failed to get subdir") } httpfiles = http.FS(subdir) } fileServer(mux, "/static", httpfiles) // Declare the root resource and a GET operation on it. - humucli.Resource("/health").Get("get-health", "Get Health of the Service", + Server.Resource("/health").Get("get-health", "Get Health of the Service", responses.OK().ContentType("application/json"), responses.OK().Headers("Content-Type"), responses.OK().Model(health.CheckerResult{}), @@ -197,7 +251,7 @@ func main() { ctx.WriteModel(status, test) }) - humucli.Resource("/config/frontend").Get("get-config", "Get Config of the Service", + Server.Resource("/config/frontend").Get("get-config", "Get Config of the Service", responses.OK().ContentType("application/json"), responses.OK().Headers("Content-Type"), responses.OK().Model(&mouthpiece.FEConfig{}), @@ -205,7 +259,7 @@ func main() { ctx.WriteModel(http.StatusOK, mouthpiece.GetFEConfig()) }) - v1api := humucli.Resource("/v1") + v1api := Server.Resource("/v1") v1api.Middleware(m.Trace) v1api.Middleware(p.Update()) @@ -215,10 +269,10 @@ func main() { responses.OK().Model(&msg.MessageResult{}), responses.NotFound().ContentType("application/json"), ).Run(func(ctx huma.Context, input msg.Message) { - Log.Info("Recieved Message", "message", input) - if app.AppExists(input.AppName) { + log.Log.Info("Recieved Message", "message", input) + if app.AppExists(ctx, input.AppName) { if err := input.ProcessMessage(); err == nil { - mouthpiece.RouteMessage(&input) + mouthpiece.RouteMessage(ctx, &input) ctx.WriteModel(http.StatusOK, input.Result) } else { ctx.WriteError(http.StatusInternalServerError, err.Error()) @@ -228,30 +282,11 @@ func main() { } }) - auth.AuthService.AddResourceURL("/v1/apps/", "apigroup:apps") - appapi := v1api.SubResource("/apps/") - appapi.Get("get-apps", "Get A List of Applications", - responses.OK().ContentType("application/json"), - responses.OK().Headers("Set-Cookie"), - responses.OK().Model([]app.App{}), - ).Run(func(ctx huma.Context) { - ctx.WriteModel(http.StatusOK, app.GetApps()) - }) - appapi.Put("create-app", "Create a Application", - responses.OK().ContentType("application/json"), - responses.OK().Headers("Set-Cookie"), - responses.OK().Model(&app.App{}), - responses.NotAcceptable().ContentType("application/json"), - responses.NotAcceptable().Headers("Set-Cookie"), - ).Run(func(ctx huma.Context, input struct { - Body app.AppDetails - }) { - if app, err := app.CreateApp(input.Body); err != nil { - ctx.WriteError(http.StatusNotAcceptable, "Database Error", err) - } else { - ctx.WriteModel(http.StatusOK, app) - } - }) + + if err := app.InitializeAppRestAPI(v1api); err != nil { + log.Log.Error(err, "Failed to initialize App Rest API") + } + auth.AuthService.AddResourceURL("/v1/users/", "apigroup:users") userapi := v1api.SubResource("/users/") @@ -260,7 +295,7 @@ func main() { responses.OK().Headers("Set-Cookie"), responses.OK().Model([]user.User{}), ).Run(func(ctx huma.Context) { - ctx.WriteModel(http.StatusOK, user.GetUsers()) + ctx.WriteModel(http.StatusOK, user.GetUsers(ctx)) }) auth.AuthService.AddResourceURL("/v1/users/{userid}/transports/", "apigroup:users") @@ -273,7 +308,7 @@ func main() { ).Run(func(ctx huma.Context, input struct { User uint `path:"userid"` }) { - if user, err := user.GetUserByID(input.User); err != nil { + if user, err := user.GetUserByID(ctx, input.User); err != nil { ctx.WriteError(http.StatusNotFound, "User Not Found", err) } else { var transport []string @@ -290,11 +325,12 @@ func main() { responses.OK().Headers("Set-Cookie"), responses.OK().Model(transport.TransportConfig{}), responses.NotFound().ContentType("application/json"), + responses.NotFound().Headers("Set-Cookie"), ).Run(func(ctx huma.Context, input struct { User uint `path:"userid"` Transport string `path:"transportid"` }) { - if user, err := user.GetUserByID(input.User); err != nil { + if user, err := user.GetUserByID(ctx, input.User); err != nil { ctx.WriteError(http.StatusNotFound, "User Not Found", err) } else { ok := false @@ -316,9 +352,9 @@ func main() { responses.OK().Headers("Set-Cookie"), responses.OK().Model([]string{}), ).Run(func(ctx huma.Context) { - ctx.WriteModel(http.StatusOK, transport.GetTransports()) + ctx.WriteModel(http.StatusOK, transport.GetTransports(ctx)) }) // Run the CLI. When passed no arguments, it starts the server. - humucli.Run() + Server.Run() }