commit 33235d8f1ee6599e322a02d0c85f1019e496ba11 Author: Shane C Date: Sun Nov 3 15:33:08 2024 -0500 Initial Commit diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..3fdcee9 --- /dev/null +++ b/.air.toml @@ -0,0 +1,24 @@ +root = "." +tmp_dir = "tmp" + +[build] +exclude_dir = ["node_modules","build","test-results","tests","playwright-report","tmp"] +exclude_regex = ["_templ\\.go","_test\\.go"] + +pre_cmd = ["rm -rf web/assets/dist/js", "templ generate", "go run -tags dev *.go generate assets", "bun run tailwindcss -i ./web/assets/css/styles.css -o ./web/assets/dist/css/styles.css"] +cmd = "go build -tags dev -o tmp/omnibill_dev *.go" +bin = "tmp/omnibill_dev" +args_bin = ["run"] + +log = "air.log" +delay = 1000 # ms +stop_on_error = true +send_interrupt = false +kill_delay = 500 # ms + + +[misc] +clean_on_exit = true + +[screen] +keep_scroll = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cdcc360 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Configs +config.toml + +# IDE +.vscode +.idea +.fleet + +# Build +build + +# Packages +node_modules + +# Dist +dist + +# Temp +tmp + +# Compiled templates +*_templ.go + +# Logs +logs + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..b733856 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,13 @@ +# You can override the included template(s) by including variable overrides +# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings +# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/pipeline/#customization +# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings +# Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings +# Note that environment variables can be set in several places +# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence +stages: + - test +sast: + stage: test +include: + - template: Security/SAST.gitlab-ci.yml diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..d644c36 Binary files /dev/null and b/bun.lockb differ diff --git a/cmd/assets.go b/cmd/assets.go new file mode 100644 index 0000000..e984d8e --- /dev/null +++ b/cmd/assets.go @@ -0,0 +1,118 @@ +//go:build dev + +/* +Package cmd +Copyright © 2024 Shane C. +*/ +package cmd + +import ( + "errors" + "github.com/evanw/esbuild/pkg/api" + "github.com/kr/pretty" + "io" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" +) + +var ignoredAssetExtensions = []string{ + ".css", + ".js", + ".ts", +} + +// assetsCmd represents the assets command +var assetsCmd = &cobra.Command{ + Use: "assets", + Short: "Generates assets needed for webserver", + Run: func(cmd *cobra.Command, args []string) { + err := filepath.Walk("./web/assets", func(path string, info os.FileInfo, err error) error { + + if info.IsDir() { + return nil + } + + relPath, _ := strings.CutPrefix(path, "web/assets/") + + if strings.HasPrefix(relPath, "dist") { + return nil + } + + for _, ext := range ignoredAssetExtensions { + if strings.HasSuffix(info.Name(), ext) { + return nil + } + } + + distFolder, _ := strings.CutSuffix(relPath, info.Name()) + + if err := os.MkdirAll("web/assets/dist/"+distFolder, 0600); err != nil && !errors.Is(err, fs.ErrExist) { + log.Fatalln(err) + } + + copyFile(path, filepath.Join("web/assets/dist/"+distFolder, info.Name())) + + return nil + + }) + if err != nil { + log.Fatalln(err) + } + + result := api.Build(api.BuildOptions{ + Format: api.FormatESModule, + EntryPoints: []string{"./web/assets/js/*.ts", "./web/assets/js/**/*.ts"}, + Outdir: "./web/assets/dist/js", + Sourcemap: api.SourceMapLinked, + MinifyWhitespace: true, + MinifyIdentifiers: true, + MinifySyntax: true, + Splitting: true, + Write: true, + TreeShaking: api.TreeShakingTrue, + Bundle: true, + }) + if len(result.Errors) != 0 { + pretty.Println(result.Errors) + os.Exit(1) + } + }, +} + +func init() { + generateCmd.AddCommand(assetsCmd) +} + +func copyFile(sourcePath, destinationPath string) { + sourcePath = filepath.Clean(sourcePath) + sourceFile, err := os.Open(sourcePath) + if err != nil { + log.Fatalln(err) + } + defer sourceFile.Close() + + destinationPath = filepath.Clean(destinationPath) + destinationFile, err := os.Create(destinationPath) + if err != nil { + log.Fatalln(err) + } + defer destinationFile.Close() + + _, err = io.Copy(destinationFile, sourceFile) + if err != nil { + log.Fatalln(err) + } + + if err := sourceFile.Close(); err != nil { + log.Fatalln(err) + } + if err := destinationFile.Close(); err != nil { + log.Fatalln(err) + } + +} diff --git a/cmd/generate.go b/cmd/generate.go new file mode 100644 index 0000000..975789d --- /dev/null +++ b/cmd/generate.go @@ -0,0 +1,21 @@ +//go:build dev + +/* +Package cmd +Copyright © 2024 Shane C. +*/ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// generateCmd represents the generate command +var generateCmd = &cobra.Command{ + Use: "generate", + Short: "Generates things for the panel", +} + +func init() { + rootCmd.AddCommand(generateCmd) +} diff --git a/cmd/handler.go b/cmd/handler.go new file mode 100644 index 0000000..0e139d6 --- /dev/null +++ b/cmd/handler.go @@ -0,0 +1,355 @@ +//go:build dev + +/* +Package cmd +Copyright © 2024 Shane C. +*/ +package cmd + +import ( + "bytes" + "errors" + "fmt" + "github.com/spf13/cobra" + "gitlab.com/omnibill/linux" + "gitlab.com/omnibill/tui/confirmation" + "gitlab.com/omnibill/tui/textinput" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + "text/template" +) + +type templateData struct { + PackagePath string + UpperName string + Name string + Path string + RequireAuth bool + GetView bool + GetNoView bool + Post bool + Put bool + Delete bool + Options bool + Head bool + Patch bool +} + +// handlerCmd represents the handler command +var handlerCmd = &cobra.Command{ + Use: "handler", + Short: "A brief description of your command", + Run: func(cmd *cobra.Command, args []string) { + tmplFile, err := templates.ReadFile("templates/handler.go.tmpl") + if err != nil { + log.Fatalln(err) + } + + viewFile, err := templates.ReadFile("templates/view.go.tmpl") + if err != nil { + log.Fatalln(err) + } + + importsFile, err := templates.ReadFile("templates/imports.go.tmpl") + if err != nil { + log.Fatalln(err) + } + + handlerTempl, err := template.New("handler").Parse(string(tmplFile)) + if err != nil { + log.Fatalln(err) + } + + viewTempl, err := template.New("view").Parse(string(viewFile)) + if err != nil { + log.Fatalln(err) + } + + importsTempl, err := template.New("imports").Parse(string(importsFile)) + if err != nil { + log.Fatalln(err) + } + + tmplData := templateData{} + + inputHandlerPath, err := textinput.New(textinput.InputData{ + Question: "Path of handler?", + }) + if err != nil { + log.Fatalln(err) + } + + pathSplit := strings.Split(*inputHandlerPath, "/") + + tmplData.UpperName = cases.Title(language.AmericanEnglish).String(pathSplit[len(pathSplit)-1]) + tmplData.Name = pathSplit[len(pathSplit)-1] + tmplData.Path = *inputHandlerPath + + if len(pathSplit) > 1 { + tmplData.PackagePath = pathSplit[len(pathSplit)-2] + } else { + tmplData.PackagePath = strings.Join(pathSplit, "") + } + + hasView, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a view?", + }) + if err != nil { + log.Fatalln(err) + } + + if *hasView { + tmplData.GetView = true + } else { + hasGetView, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a GET handler?", + }) + if err != nil { + log.Fatalln(err) + } + tmplData.GetNoView = *hasGetView + } + + hasAuth, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a AUTH handler?", + }) + if err != nil { + log.Fatalln(err) + } + tmplData.RequireAuth = *hasAuth + + hasPost, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a POST handler?", + }) + if err != nil { + log.Fatalln(err) + } + tmplData.Post = *hasPost + + hasPut, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a PUT handler?", + }) + if err != nil { + log.Fatalln(err) + } + tmplData.Put = *hasPut + + hasDelete, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a DELETE handler?", + }) + if err != nil { + log.Fatalln(err) + } + tmplData.Delete = *hasDelete + + hasOptions, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a OPTIONS handler?", + }) + if err != nil { + log.Fatalln(err) + } + tmplData.Options = *hasOptions + + hasHead, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a HEAD handler?", + }) + if err != nil { + log.Fatalln(err) + } + tmplData.Head = *hasHead + + hasPatch, err := confirmation.New(confirmation.InputData{ + Question: "Does this handler need a PATCH handler?", + }) + if err != nil { + log.Fatalln(err) + } + tmplData.Patch = *hasPatch + + cwd, err := os.Getwd() + if err != nil { + log.Fatalln(err) + } + + handlerDir := filepath.Join(cwd, "web/handlers") + viewDir := filepath.Join(cwd, "web/views") + + if *hasView { + for i, _ := range pathSplit { + isLast := i == len(pathSplit)-1 + path := filepath.Join(append([]string{viewDir}, pathSplit[0:i+1]...)...) + + if _, err := os.Lstat(path); err != nil && !errors.Is(err, fs.ErrNotExist) { + log.Fatalln(err) + } else if err != nil && errors.Is(err, fs.ErrNotExist) { + if err := os.MkdirAll(path, 0740); err != nil { + log.Fatalln(err) + } + } + + if isLast { + viewFileOut, err := os.Create(path + "/show.templ") + if err != nil { + log.Fatalln(err) + } + defer viewFileOut.Close() + + var buffer bytes.Buffer + if err := viewTempl.Execute(&buffer, tmplData); err != nil { + log.Fatalln(err) + } + if _, err := viewFileOut.Write(buffer.Bytes()); err != nil { + log.Fatalln(err) + } + viewFileOut.Close() + } + } + } + + templCommand, err := linux.NewCommand(linux.CommandOptions{ + Env: map[string]string{ + "PATH": os.Getenv("PATH"), + }, + Command: "templ", + Args: []string{"generate"}, + Shell: "/bin/bash", + }) + if err != nil { + log.Fatalln(err) + } + + if err := templCommand.Run(); err != nil { + log.Fatalln(err) + } + + for i, _ := range pathSplit { + isLast := i == len(pathSplit)-1 + path := filepath.Join(append([]string{handlerDir}, pathSplit[0:i+1]...)...) + + if _, err := os.Lstat(path + ".go"); err != nil && !errors.Is(err, fs.ErrNotExist) { + log.Fatalln(err) + } else if err == nil { + if err := os.MkdirAll(path, 0740); err != nil { + log.Fatalln(err) + } + if err := os.Rename(path+".go", path+"/index.go"); err != nil { + log.Fatalln(err) + } + } + + if _, err := os.Lstat(path); err != nil && !errors.Is(err, fs.ErrNotExist) { + log.Fatalln(err) + } else if err != nil && errors.Is(err, fs.ErrNotExist) && !isLast { + if err := os.MkdirAll(path, 0740); err != nil { + log.Fatalln(err) + } + } else if err != nil && errors.Is(err, fs.ErrNotExist) && isLast { + handlerFileOut, err := os.Create(path + ".go") + if err != nil { + log.Fatalln(err) + } + defer handlerFileOut.Close() + + var buffer bytes.Buffer + if err := handlerTempl.Execute(&buffer, tmplData); err != nil { + log.Fatalln(err) + } + if _, err := handlerFileOut.Write(buffer.Bytes()); err != nil { + log.Fatalln(err) + } + handlerFileOut.Close() + } else if err == nil && isLast { + handlerFileOut, err := os.Create(path + "/index.go") + if err != nil { + log.Fatalln(err) + } + defer handlerFileOut.Close() + + var buffer bytes.Buffer + if err := handlerTempl.Execute(&buffer, tmplData); err != nil { + log.Fatalln(err) + } + if _, err := handlerFileOut.Write(buffer.Bytes()); err != nil { + log.Fatalln(err) + } + handlerFileOut.Close() + } + + } + + fmt.Println("Generating Imports") + + var imports []string + + if err := filepath.WalkDir(handlerDir, func(path string, d fs.DirEntry, err error) error { + if strings.HasSuffix(d.Name(), ".go") { + folder := filepath.Dir(path) + isFound := false + + if folder == handlerDir { + return nil + } + + output, _ := strings.CutPrefix(folder, cwd) + + for _, handlerImport := range imports { + if handlerImport == "omnibill.net/omnibill"+output { + isFound = true + } + } + + if !isFound { + imports = append(imports, "omnibill.net/omnibill"+output) + } + } + + return nil + }); err != nil { + log.Fatalln(err) + } + + if len(imports) != 0 { + importFileOut, err := os.Create(handlerDir + "/imports.go") + if err != nil { + log.Fatalln(err) + } + defer importFileOut.Close() + + var buffer bytes.Buffer + if err := importsTempl.Execute(&buffer, map[string]interface{}{ + "Imports": imports, + }); err != nil { + log.Fatalln(err) + } + if _, err := importFileOut.Write(buffer.Bytes()); err != nil { + log.Fatalln(err) + } + importFileOut.Close() + } + + fmt.Println("Go formatting") + + fmtCommand, err := linux.NewCommand(linux.CommandOptions{ + Command: "go", + Args: []string{"fmt", "./..."}, + Shell: "/bin/bash", + }) + if err != nil { + log.Fatalln(err) + } + + if err := fmtCommand.Run(); err != nil { + log.Fatalln(err) + } + + }, +} + +func init() { + generateCmd.AddCommand(handlerCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..f2ef5d9 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,66 @@ +/* +Package cmd +Copyright © 2024 Shane C. +*/ +package cmd + +import ( + "embed" + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "os" +) + +var cfgFile string + +//go:embed templates/* +var templates embed.FS + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "omnibill", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + Run: func(cmd *cobra.Command, args []string) { + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.omnibill.yaml)") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + viper.SetConfigType("toml") + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + viper.AddConfigPath("/etc/omnibill") + viper.SetConfigName("config") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/cmd/templates/handler.go.tmpl b/cmd/templates/handler.go.tmpl new file mode 100644 index 0000000..fdc7f81 --- /dev/null +++ b/cmd/templates/handler.go.tmpl @@ -0,0 +1,81 @@ +package {{.PackagePath}} + +import ( + "github.com/go-webauthn/webauthn/webauthn" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/uptrace/bun" + "go.uber.org/zap" + "omnibill.net/omnibill/web/utils" + {{- if .GetView }} + PAGE_VIEW "omnibill.net/omnibill/web/views/{{.Path}}" + {{ end }} +) + +func init() { + utils.Handlers = append(utils.Handlers, &{{.UpperName}}Handler{ + Path: "{{.Path}}", + }) +} + +type {{.UpperName}}Handler struct { + Path string {{ if .RequireAuth }}`omnibill:"requireAuth"`{{ end }} + Db *bun.DB + Logger *zap.Logger + + AuthSessionStore *session.Store + SessionStore *session.Store + + Session *session.Session + AuthSession *session.Session + + WebAuthn *webauthn.WebAuthn +} + +{{- if .GetView }} +func (h {{.UpperName}}Handler) Get(c *fiber.Ctx) error { + return utils.Render(c, PAGE_VIEW.Show()) +} +{{ end -}} + +{{- if .GetNoView }} +func (h {{.UpperName}}Handler) Get(c *fiber.Ctx) error { + return nil +} +{{ end -}} + +{{- if .Post }} +func (h {{.UpperName}}Handler) Post(c *fiber.Ctx) error { + return nil +} +{{ end -}} + +{{- if .Put }} +func (h {{.UpperName}}Handler) Put(c *fiber.Ctx) error { + return nil +} +{{ end -}} + +{{- if .Delete }} +func (h {{.UpperName}}Handler) Delete(c *fiber.Ctx) error { + return nil +} +{{ end -}} + +{{- if .Options }} +func (h {{.UpperName}}Handler) Options(c *fiber.Ctx) error { + return nil +} +{{ end -}} + +{{- if .Head }} +func (h {{.UpperName}}Handler) Head(c *fiber.Ctx) error { + return nil +} +{{ end -}} + +{{- if .Patch }} +func (h {{.UpperName}}Handler) Patch(c *fiber.Ctx) error { + return nil +} +{{ end -}} diff --git a/cmd/templates/imports.go.tmpl b/cmd/templates/imports.go.tmpl new file mode 100644 index 0000000..6f91345 --- /dev/null +++ b/cmd/templates/imports.go.tmpl @@ -0,0 +1,11 @@ +// Code generated by Omnibill - DO NOT EDIT + +package handlers + +{{ if ne (len .Imports) 0 }} +import ( + {{- range .Imports }} + _ "{{.}}" + {{ end -}} +) +{{ end }} \ No newline at end of file diff --git a/cmd/templates/view.go.tmpl b/cmd/templates/view.go.tmpl new file mode 100644 index 0000000..2e7314b --- /dev/null +++ b/cmd/templates/view.go.tmpl @@ -0,0 +1,9 @@ +package {{.PackagePath}} + +import "omnibill.net/omnibill/web/views/layouts" + +templ Show() { + @layouts.Base(nil) { +

{{.Name}}

+ } +} \ No newline at end of file diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..004ac20 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,33 @@ +[omnibill] +debug = false +domain = "" +display_name = "Omnibill" + +# Webserver Settings +[omnibill.webserver] +use_https = false +proxy = "" +port = 9000 + +# Database Settings +[omnibill.database] +host = "127.0.0.1" +port = 5432 +database = "omnibill" + +username = "omnibill" +password = "" + +# Mailer Settings +[omnibill.mailer] +enabled = false + +host = "127.0.0.1" +encryption = "tls" # Can be "none", "tls", or "starttls" +port = 587 + +username = "" +password = "" + +from_name = "Omnibill Panel" +from_addr = "omnibill@example.com" diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..5790540 --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,96 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; + +export default tseslint.config({ + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ], + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + parser: tsParser, + }, + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + 'arrow-spacing': ['warn', { + before: true, + after: true, + }], + + 'brace-style': ['error', 'stroustrup', { + allowSingleLine: true, + }], + + 'comma-dangle': ['error', 'always-multiline'], + 'comma-spacing': 'error', + 'comma-style': 'error', + curly: ['error', 'multi-line', 'consistent'], + 'dot-location': ['error', 'property'], + 'handle-callback-err': 'off', + indent: ['error', 'tab'], + 'keyword-spacing': 'error', + + 'max-nested-callbacks': ['error', { + max: 4, + }], + + 'max-statements-per-line': ['error', { + max: 2, + }], + + 'no-console': 'off', + 'no-empty-function': 'error', + 'no-floating-decimal': 'error', + 'no-inline-comments': 'error', + 'no-lonely-if': 'error', + 'no-multi-spaces': 'error', + + 'no-multiple-empty-lines': ['error', { + max: 2, + maxEOF: 1, + maxBOF: 0, + }], + + 'no-shadow': ['error', { + allow: ['err', 'resolve', 'reject'], + }], + + 'no-trailing-spaces': ['error'], + 'no-var': 'error', + + '@typescript-eslint/no-unused-vars': ['error', { + argsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + }], + + 'no-unused-vars': 'off', + 'object-curly-spacing': ['error', 'always'], + 'prefer-const': 'error', + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'space-before-blocks': 'error', + + 'space-before-function-paren': ['error', { + anonymous: 'never', + named: 'never', + asyncArrow: 'always', + }], + + 'space-in-parens': 'error', + 'space-infix-ops': 'error', + 'space-unary-ops': 'error', + 'spaced-comment': 'error', + yoda: 'error', + }, + }, +); \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6b91ef0 --- /dev/null +++ b/go.mod @@ -0,0 +1,75 @@ +module omnibill.net/omnibill + +go 1.23.2 + +require ( + github.com/go-webauthn/webauthn v0.11.2 + github.com/gofiber/fiber/v2 v2.52.5 + github.com/kr/pretty v0.3.1 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + github.com/uptrace/bun v1.2.5 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/a-h/templ v0.2.793 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/evanw/esbuild v0.24.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-git/go-billy/v5 v5.6.0 // indirect + github.com/go-webauthn/x v0.1.14 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/gofiber/storage/postgres/v3 v3.0.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/go-tpm v0.9.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/nicksnyder/go-i18n/v2 v2.4.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/ulikunitz/xz v0.5.12 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.57.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + gitlab.com/omnibill/archiver v1.0.0 // indirect + gitlab.com/omnibill/linux v1.0.0 // indirect + gitlab.com/omnibill/tui v1.0.1 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a03ceaf --- /dev/null +++ b/go.sum @@ -0,0 +1,171 @@ +github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY= +github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/evanw/esbuild v0.24.0 h1:GZ78naTLp7FKr+K7eNuM/SLs5maeiHYRPsTg6kmdsSE= +github.com/evanw/esbuild v0.24.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= +github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= +github.com/go-webauthn/webauthn v0.11.2 h1:Fgx0/wlmkClTKlnOsdOQ+K5HcHDsDcYIvtYmfhEOSUc= +github.com/go-webauthn/webauthn v0.11.2/go.mod h1:aOtudaF94pM71g3jRwTYYwQTG1KyTILTcZqN1srkmD0= +github.com/go-webauthn/x v0.1.14 h1:1wrB8jzXAofojJPAaRxnZhRgagvLGnLjhCAwg3kTpT0= +github.com/go-webauthn/x v0.1.14/go.mod h1:UuVvFZ8/NbOnkDz3y1NaxtUN87pmtpC1PQ+/5BBQRdc= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= +github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/storage/postgres/v3 v3.0.0 h1:zV2e54PmCO1isZcnWufZ6DlId2FwsRAJgW7WxOK+ei8= +github.com/gofiber/storage/postgres/v3 v3.0.0/go.mod h1:TB7QJeilUS/FGvbwis6lY4tcGOLdAHJt7M11GNGXobA= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= +github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nicksnyder/go-i18n/v2 v2.4.1 h1:zwzjtX4uYyiaU02K5Ia3zSkpJZrByARkRB4V3YPrr0g= +github.com/nicksnyder/go-i18n/v2 v2.4.1/go.mod h1:++Pl70FR6Cki7hdzZRnEEqdc2dJt+SAGotyFg/SvZMk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/uptrace/bun v1.2.5 h1:gSprL5xiBCp+tzcZHgENzJpXnmQwRM/A6s4HnBF85mc= +github.com/uptrace/bun v1.2.5/go.mod h1:vkQMS4NNs4VNZv92y53uBSHXRqYyJp4bGhMHgaNCQpY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.57.0 h1:Xw8SjWGEP/+wAAgyy5XTvgrWlOD1+TxbbvNADYCm1Tg= +github.com/valyala/fasthttp v1.57.0/go.mod h1:h6ZBaPRlzpZ6O3H5t2gEk1Qi33+TmLvfwgLLp0t9CpE= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +gitlab.com/omnibill/archiver v1.0.0 h1:5cwP2NoUF+c2zzPYmS9Ds1EY+PA5Cl7oqWx6dV6iR68= +gitlab.com/omnibill/archiver v1.0.0/go.mod h1:SWYvKtq4CX6j196ZYPiOwBKDPJMLXV1cUfBWKqyroLo= +gitlab.com/omnibill/linux v1.0.0 h1:gvrjxRSSY3Mo6BoIf7LQ5wYtl0DxzB17iazvJP4LbbM= +gitlab.com/omnibill/linux v1.0.0/go.mod h1:fngJPKncBHcTzbMqr6rRT5AEnO4X/hzm1g2/TFKr5j8= +gitlab.com/omnibill/tui v1.0.0 h1:YBqqoS9kqJehP1TiofAdleQocXCp+nSO6D4942tK8dw= +gitlab.com/omnibill/tui v1.0.0/go.mod h1:RNOO1V8WPiV65qZ9LSHgakhSJcC8VS1JQLwJvjM1Dug= +gitlab.com/omnibill/tui v1.0.1 h1:aj5ULPEkgcuE8saaAB17onZsjBY9RuJxrKo5e5ZYkiQ= +gitlab.com/omnibill/tui v1.0.1/go.mod h1:RNOO1V8WPiV65qZ9LSHgakhSJcC8VS1JQLwJvjM1Dug= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..bf0fdb4 --- /dev/null +++ b/main.go @@ -0,0 +1,12 @@ +/* +Copyright © 2024 Shane C. +*/ +package main + +import ( + "omnibill.net/omnibill/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..f64448f --- /dev/null +++ b/models/models.go @@ -0,0 +1,80 @@ +package models + +import ( + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/uptrace/bun" +) + +type User struct { + bun.BaseModel `bun:"table:users"` + ID string `bun:",pk"` + + Username string + Email string + Hash string + + FirstName string `bun:"nullzero"` + LastName string `bun:",nullzero"` + DisplayName string `bun:",nullzero"` + + LoginMethods []*UserLoginMethod `bun:"rel:has-many,join:id=user_id"` + Logs []*UserLog `bun:"rel:has-many,join:id=user_id"` +} + +type UserLoginMethod struct { + bun.BaseModel `bun:"table:users_login_methods"` + ID int64 `bun:",pk,autoincrement"` + UserID string + + Type string + Name string + WebAuthn *webauthn.Credential `bun:",nullzero,type:jsonb"` +} + +func (u User) WebAuthnID() []byte { + return []byte(u.ID) +} + +func (u User) WebAuthnName() string { + return u.Username +} + +func (u User) WebAuthnDisplayName() string { + if len(u.DisplayName) != 0 { + return u.DisplayName + } else { + return u.FirstName + " " + u.LastName + } +} + +func (u User) WebAuthnCredentials() []webauthn.Credential { + var credentials []webauthn.Credential + for _, l := range u.LoginMethods { + if l.WebAuthn != nil { + credentials = append(credentials, *l.WebAuthn) + } + } + return credentials +} + +func (u User) WebAuthnCredentialExcludeList() []protocol.CredentialDescriptor { + var excludeList []protocol.CredentialDescriptor + + webauthnCreds := u.WebAuthnCredentials() + for _, cred := range webauthnCreds { + descriptor := protocol.CredentialDescriptor{ + Type: protocol.PublicKeyCredentialType, + CredentialID: cred.ID, + } + excludeList = append(excludeList, descriptor) + } + + return excludeList +} + +type UserLog struct { + bun.BaseModel `bun:"table:user_logs"` + ID int64 `bun:",pk,autoincrement"` + UserID int64 `bun:",pk"` +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..180da9b --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "omnibill", + "author": "Shane C.", + "type": "module", + "scripts": { + "lint": "eslint --flag unstable_ts_config --config eslint.config.ts . ", + "lint:fix": "eslint --flag unstable_ts_config --config eslint.config.ts . --fix" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.14.0", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@types/alpinejs": "^3.13.10", + "@types/bun": "latest", + "@types/eslint__js": "^8.42.3", + "@typescript-eslint/parser": "^8.12.2", + "autoprefixer": "^10.4.20", + "daisyui": "^4.12.14", + "esbuild": "^0.24.0", + "eslint": "^9.14.0", + "globals": "^15.11.0", + "jiti": "^2.4.0", + "postcss": "^8.4.47", + "postcss-load-config": "^6.0.1", + "typescript": "^5.6.3", + "typescript-eslint": "^8.12.2" + }, + "dependencies": { + "@tiptap/core": "^2.9.1", + "@tiptap/extension-bold": "^2.9.1", + "@tiptap/extension-document": "^2.9.1", + "@tiptap/extension-heading": "^2.9.1", + "@tiptap/extension-italic": "^2.9.1", + "@tiptap/extension-link": "^2.9.1", + "@tiptap/extension-paragraph": "^2.9.1", + "@tiptap/extension-strike": "^2.9.1", + "@tiptap/extension-text": "^2.9.1", + "@tiptap/extension-underline": "^2.9.1", + "@tiptap/pm": "^2.9.1", + "alpinejs": "^3.14.3", + "email-validator": "^2.0.4" + } +} \ No newline at end of file diff --git a/postcss.config.ts b/postcss.config.ts new file mode 100644 index 0000000..d9106f8 --- /dev/null +++ b/postcss.config.ts @@ -0,0 +1,8 @@ +import type { Config } from 'postcss-load-config'; + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} satisfies Config; \ No newline at end of file diff --git a/shared/hash.go b/shared/hash.go new file mode 100644 index 0000000..3be5505 --- /dev/null +++ b/shared/hash.go @@ -0,0 +1,79 @@ +package shared + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "errors" + "golang.org/x/crypto/argon2" + "runtime" + "strings" +) + +type Hash struct { + Hash []byte + Salt []byte +} + +func (h *Hash) String() string { + return base64.StdEncoding.EncodeToString(h.Salt) + ";" + base64.StdEncoding.EncodeToString(h.Hash) +} + +func NewHash(pass string, salt []byte) (hash *Hash, err error) { + + if salt == nil { + salt = make([]byte, 16) + _, err = rand.Read(salt) + if err != nil { + return nil, err + } + } + + var parallelism uint8 + + if runtime.NumCPU() > 255 { + parallelism = 255 + } else { + parallelism = uint8(runtime.NumCPU()) // #nosec G115 -- False positive, if this does happen, blame radiation. + } + + argonHash := argon2.IDKey([]byte(pass), salt, 6, 64*1024, parallelism, 45) + + return &Hash{ + Salt: salt, + Hash: argonHash, + }, nil +} + +var ErrInvalidHashStr = errors.New("invalid hash string") + +func CompareHash(hash string, pass string) (matches bool, err error) { + + hashSplit := strings.Split(hash, ";") + + if len(hashSplit) != 2 { + return false, ErrInvalidHashStr + } + + salt, err := base64.StdEncoding.DecodeString(hashSplit[0]) + if err != nil { + return false, err + } + + decodedHash, err := base64.StdEncoding.DecodeString(hashSplit[1]) + if err != nil { + return false, err + } + + hashInfo, err := NewHash(pass, salt) + if err != nil { + return false, err + } + + if !bytes.Equal(decodedHash, hashInfo.Hash) { + return false, nil + } + + return true, nil + +} diff --git a/shared/i18n.go b/shared/i18n.go new file mode 100644 index 0000000..ed6bc25 --- /dev/null +++ b/shared/i18n.go @@ -0,0 +1,36 @@ +package shared + +import "github.com/nicksnyder/go-i18n/v2/i18n" + +var I18nLocalizer *i18n.Localizer + +type TranslateOptions struct { + TemplateData interface{} + PluralCount interface{} + Zero string + One string + Two string + Few string + Many string + Other string +} + +func T(id string, options *TranslateOptions) string { + + localizedStr := I18nLocalizer.MustLocalize(&i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: id, + Zero: options.Zero, + One: options.One, + Two: options.Two, + Few: options.Few, + Many: options.Many, + Other: options.Other, + }, + TemplateData: options.TemplateData, + PluralCount: options.PluralCount, + }) + + return localizedStr + +} diff --git a/shared/json.go b/shared/json.go new file mode 100644 index 0000000..ee69ba3 --- /dev/null +++ b/shared/json.go @@ -0,0 +1,44 @@ +package shared + +import ( + "bytes" + "io" + "io/fs" + "os" + + "github.com/goccy/go-json" +) + +func LoadJSONFileFS(fsys fs.FS, fileName string, value interface{}) error { + file, err := fs.ReadFile(fsys, fileName) + if err != nil { + return err + } + + fileBuffer := bytes.NewReader(file) + + if err := json.NewDecoder(fileBuffer).Decode(&value); err != nil && err != io.EOF { + return err + } + + clear(file) + + return nil +} + +func LoadJSONFile(fileName string, value interface{}) error { + file, err := os.ReadFile(fileName) + if err != nil { + return err + } + + fileBuffer := bytes.NewReader(file) + + if err := json.NewDecoder(fileBuffer).Decode(&value); err != nil && err != io.EOF { + return err + } + + clear(file) + + return nil +} diff --git a/shared/json_test.go b/shared/json_test.go new file mode 100644 index 0000000..05480ca --- /dev/null +++ b/shared/json_test.go @@ -0,0 +1,23 @@ +package shared + +import ( + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestLoadJsonFile(t *testing.T) { + err := os.WriteFile("test_out/test.json", []byte(`{"name": "test", "test": 1}`), os.ModePerm) + assert.NoError(t, err) + + var value struct { + Name string `json:"name"` + Test int `json:"test"` + } + + err = LoadJSONFile("test_out/test.json", &value) + assert.NoError(t, err) + + assert.NotEmpty(t, value.Test) + assert.NotEmpty(t, value.Name) +} diff --git a/shared/postgres.go b/shared/postgres.go new file mode 100644 index 0000000..92c3a48 --- /dev/null +++ b/shared/postgres.go @@ -0,0 +1,24 @@ +package shared + +import ( + "github.com/spf13/viper" + "net/url" +) + +func GetPostgresURI() string { + postgresURI := url.URL{ + Scheme: "postgresql", + User: url.UserPassword(viper.GetString("database.user"), viper.GetString("database.password")), + Host: viper.GetString("database.host"), + Path: viper.GetString("database.database"), + } + + values := postgresURI.Query() + + values.Add("sslmode", "disable") + values.Add("timezone", viper.GetString("database.tz")) + + postgresURI.RawQuery = values.Encode() + + return postgresURI.String() +} diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..0a83854 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,20 @@ +import typography from '@tailwindcss/typography'; +import type { Config } from 'tailwindcss'; +import forms from '@tailwindcss/forms'; +import daisyui from 'daisyui'; + +export default { + content: [ + './web/views/**/*.templ', + ], + safelist: [ + 'editor', + ], + darkMode: 'class', + plugins: [ + typography, + daisyui, + forms, + ], +} satisfies Config; + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1921234 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/web/assets/css/styles.css b/web/assets/css/styles.css new file mode 100644 index 0000000..728d6a5 --- /dev/null +++ b/web/assets/css/styles.css @@ -0,0 +1,31 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + height: 100svh; + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +@layer components { + .editor { + @apply prose prose-sm sm:prose-base lg:prose-lg xl:prose-2xl m-5 focus:outline-none p-3 rounded-lg shadow-lg bg-slate-800; + } + .editor-controls button { + @apply btn btn-primary; + } + .active-editor-control { + @apply btn-secondary !important; + } +} \ No newline at end of file diff --git a/web/middleware/auth.go b/web/middleware/auth.go new file mode 100644 index 0000000..e6f16b2 --- /dev/null +++ b/web/middleware/auth.go @@ -0,0 +1,50 @@ +package middleware + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/uptrace/bun" + "go.uber.org/zap" + "omnibill.net/omnibill/models" + "reflect" +) + +func Auth(logger *zap.Logger, db *bun.DB, authSessionStore *session.Store, handler interface{}) fiber.Handler { + return func(c *fiber.Ctx) error { + if !c.IsProxyTrusted() { + return fiber.ErrUnauthorized + } + + authSession, err := authSessionStore.Get(c) + if err != nil { + return fiber.ErrUnauthorized + } + + if len(authSession.Keys()) == 0 { + return fiber.ErrUnauthorized + } + + var user models.User + userID := authSession.Get("uid").(string) + keyCount, err := db.NewSelect().Model(&user).Where("id = ?", userID).Count(c.UserContext()) + if err != nil { + logger.Error("error getting columns", zap.Error(err)) + return fiber.ErrInternalServerError + } + + if keyCount == 0 { + if err := authSession.Destroy(); err != nil { + logger.Error("error destroying session", zap.Error(err)) + return fiber.ErrInternalServerError + } + if err := authSession.Save(); err != nil { + logger.Error("error saving session", zap.Error(err)) + return fiber.ErrInternalServerError + } + return fiber.ErrUnauthorized + } + + reflect.ValueOf(handler).Elem().FieldByName("AuthSession").Set(reflect.ValueOf(authSession)) + return nil + } +} diff --git a/web/server.go b/web/server.go new file mode 100644 index 0000000..3efb2ec --- /dev/null +++ b/web/server.go @@ -0,0 +1,264 @@ +package web + +import ( + "bufio" + "embed" + "encoding/gob" + "errors" + "fmt" + "github.com/a-h/templ" + "github.com/go-webauthn/webauthn/webauthn" + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/middleware/earlydata" + "github.com/gofiber/fiber/v2/middleware/etag" + "github.com/gofiber/fiber/v2/middleware/filesystem" + "github.com/gofiber/fiber/v2/middleware/healthcheck" + "github.com/gofiber/fiber/v2/middleware/helmet" + "github.com/gofiber/fiber/v2/middleware/limiter" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/gofiber/storage/postgres/v3" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/spf13/viper" + "github.com/uptrace/bun" + "go.uber.org/zap" + "net/http" + "net/url" + "omnibill.net/omnibill/web/utils" + "omnibill.net/omnibill/web/views/layouts" + "reflect" + "strings" + "time" +) + +//go:embed assets/**/* +var assetDir embed.FS + +func Start(logger *zap.Logger, db *bun.DB, dbPool *pgxpool.Pool) { + + panelURL, err := url.Parse(viper.GetString("omnibill.domain")) + if err != nil { + logger.Fatal("error parsing panel URL", zap.Error(err)) + } + + gob.Register(&webauthn.SessionData{}) + webAuthnConfig := &webauthn.Config{ + RPDisplayName: viper.GetString("omnibill.display_name"), + RPID: panelURL.Host, + RPOrigins: []string{panelURL.String()}, + } + + webAuthn, err := webauthn.New(webAuthnConfig) + if err != nil { + logger.Fatal("error creating webauthn", zap.Error(err)) + } + + appConfig := fiber.Config{ + AppName: viper.GetString("omnibill.display_name"), + JSONEncoder: json.Marshal, + JSONDecoder: json.Unmarshal, + } + + if len(viper.GetString("omnibill.webserver.proxy")) != 0 { + switch strings.ToLower(viper.GetString("omnibill.webserver.proxy")) { + case "cloudflare", "cf": + logger.Info("grabbing trusted proxy list") + + var trustedProxies []string + + v4Req, err := http.NewRequest("GET", "https://www.cloudflare.com/ips-v4/#", nil) + if err != nil { + logger.Fatal("error creating request", zap.Error(err)) + } + + v6Req, err := http.NewRequest("GET", "https://www.cloudflare.com/ips-v6/#", nil) + if err != nil { + logger.Fatal("error creating request", zap.Error(err)) + } + + client := &http.Client{} + + v4Resp, err := client.Do(v4Req) + if err != nil { + logger.Fatal("error doing request", zap.Error(err)) + } + defer v4Resp.Body.Close() + + v4Scanner := bufio.NewScanner(v4Resp.Body) + v4Scanner.Split(bufio.ScanLines) + + for v4Scanner.Scan() { + trustedProxies = append(trustedProxies, v4Scanner.Text()) + } + + v6Resp, err := client.Do(v6Req) + if err != nil { + logger.Fatal("error doing request", zap.Error(err)) + } + defer v6Resp.Body.Close() + + v6Scanner := bufio.NewScanner(v6Resp.Body) + v6Scanner.Split(bufio.ScanLines) + + for v6Scanner.Scan() { + trustedProxies = append(trustedProxies, v6Scanner.Text()) + } + + appConfig.ProxyHeader = "X-Forwarded-For" + appConfig.TrustedProxies = trustedProxies + case "none": + default: + log.Warnf("Proxy '%s' is not supported", viper.GetString("omnibill.webserver.proxy")) + } + } + + app := fiber.New(appConfig) + app.Use(recover.New()) + app.Use(earlydata.New()) + app.Use(healthcheck.New()) + app.Use(helmet.New()) + app.Use(etag.New()) + app.Use(limiter.New(limiter.Config{ + Max: 250, + Expiration: 3 * time.Second, + LimiterMiddleware: limiter.SlidingWindow{}, + })) + app.Use("/assets", filesystem.New(filesystem.Config{ + Root: http.FS(assetDir), + PathPrefix: "assets", + Browse: false, + })) + + storage := postgres.New(postgres.Config{ + DB: dbPool, + Table: "sessions", + }) + authSessionStore := session.New(session.Config{ + Storage: storage, + }) + sessionStore := session.New(session.Config{ + KeyLookup: "cookie:osession", + }) + + for _, handler := range utils.Handlers { + handlerType := reflect.TypeOf(handler).Elem() + handlerValue := reflect.ValueOf(handler).Elem() + + pathField, ok := handlerType.FieldByName("Path") + if !ok { + fmt.Println("invalid handler") + continue + } + + var requireAuth bool + + omnibillTag := pathField.Tag.Get("omnibill") + for _, option := range strings.Split(omnibillTag, ",") { + switch option { + case "requireAuth": + requireAuth = true + } + } + + var pathHandlers []fiber.Handler + if requireAuth { + pathHandlers = append(pathHandlers, nil) + } + + handlerValue.FieldByName("Db").Set(reflect.ValueOf(db)) + handlerValue.FieldByName("AuthSessionStore").Set(reflect.ValueOf(authSessionStore)) + handlerValue.FieldByName("SessionStore").Set(reflect.ValueOf(sessionStore)) + handlerValue.FieldByName("Logger").Set(reflect.ValueOf(logger)) + handlerValue.FieldByName("WebAuthn").Set(reflect.ValueOf(webAuthn)) + + path := handlerValue.FieldByName("Path").String() + if path == "index" { + path = "" + } + path = "/" + path + + if iHandler, ok := handler.(utils.GET); ok { + pathHandlers = append(pathHandlers, iHandler.Get) + app.Get(path, func(ctx *fiber.Ctx) error { + sess, err := sessionStore.Get(ctx) + if err != nil { + return fiber.ErrInternalServerError + } + + handlerValue.FieldByName("Session").Set(reflect.ValueOf(sess)) + for _, pathHandler := range pathHandlers { + err := pathHandler(ctx) + if err != nil { + var e *fiber.Error + if errors.As(err, &e) { + return utils.Render(ctx, layouts.Error(*e), templ.WithStatus(e.Code)) + } else { + return err + } + } + } + return nil + }) + } + if iHandler, ok := handler.(utils.POST); ok { + pathHandlers = append(pathHandlers, iHandler.Post) + app.Post(path, func(ctx *fiber.Ctx) error { + return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers) + }) + } + if iHandler, ok := handler.(utils.PUT); ok { + pathHandlers = append(pathHandlers, iHandler.Put) + app.Put(path, func(ctx *fiber.Ctx) error { + return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers) + }) + } + if iHandler, ok := handler.(utils.DELETE); ok { + pathHandlers = append(pathHandlers, iHandler.Delete) + app.Delete(path, func(ctx *fiber.Ctx) error { + return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers) + }) + } + if iHandler, ok := handler.(utils.PATCH); ok { + pathHandlers = append(pathHandlers, iHandler.Patch) + app.Patch(path, func(ctx *fiber.Ctx) error { + return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers) + }) + } + if iHandler, ok := handler.(utils.OPTIONS); ok { + pathHandlers = append(pathHandlers, iHandler.Options) + app.Options(path, func(ctx *fiber.Ctx) error { + return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers) + }) + } + if iHandler, ok := handler.(utils.HEAD); ok { + pathHandlers = append(pathHandlers, iHandler.Head) + app.Head(path, func(ctx *fiber.Ctx) error { + return genericPathHandler(ctx, handlerValue, sessionStore, pathHandlers) + }) + } + } + +} + +func genericPathHandler(ctx *fiber.Ctx, handler reflect.Value, sessionStore *session.Store, handlers []fiber.Handler) error { + sess, err := sessionStore.Get(ctx) + if err != nil { + return fiber.ErrInternalServerError + } + + handler.FieldByName("Session").Set(reflect.ValueOf(sess)) + for _, pathHandler := range handlers { + err := pathHandler(ctx) + if err != nil { + var e *fiber.Error + if errors.As(err, &e) { + return err + } else { + return fiber.ErrInternalServerError + } + } + } + return nil +} diff --git a/web/utils/handlers.go b/web/utils/handlers.go new file mode 100644 index 0000000..26a5019 --- /dev/null +++ b/web/utils/handlers.go @@ -0,0 +1,33 @@ +package utils + +import "github.com/gofiber/fiber/v2" + +var Handlers []interface{} + +type GET interface { + Get(ctx *fiber.Ctx) error +} + +type POST interface { + Post(ctx *fiber.Ctx) error +} + +type PUT interface { + Put(ctx *fiber.Ctx) error +} + +type DELETE interface { + Delete(ctx *fiber.Ctx) error +} + +type HEAD interface { + Head(ctx *fiber.Ctx) error +} + +type OPTIONS interface { + Options(ctx *fiber.Ctx) error +} + +type PATCH interface { + Patch(ctx *fiber.Ctx) error +} diff --git a/web/utils/render.go b/web/utils/render.go new file mode 100644 index 0000000..82cb31d --- /dev/null +++ b/web/utils/render.go @@ -0,0 +1,15 @@ +package utils + +import ( + "github.com/a-h/templ" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func Render(c *fiber.Ctx, component templ.Component, options ...func(*templ.ComponentHandler)) error { + componentHandler := templ.Handler(component) + for _, o := range options { + o(componentHandler) + } + return adaptor.HTTPHandler(componentHandler)(c) +} diff --git a/web/utils/session.go b/web/utils/session.go new file mode 100644 index 0000000..d4b585b --- /dev/null +++ b/web/utils/session.go @@ -0,0 +1 @@ +package utils diff --git a/web/views/components/utils.templ b/web/views/components/utils.templ new file mode 100644 index 0000000..161a2df --- /dev/null +++ b/web/views/components/utils.templ @@ -0,0 +1,29 @@ +package components + +var showOnceHandle = templ.NewOnceHandle() + +type ScriptAssetOptions struct { + IsAsync bool + IsDefer bool + IsModule bool + DoImport bool +} + +templ LoadJSAsset(assetName string, opts *ScriptAssetOptions) { + if opts == nil { + + } else { + if opts.IsModule { + if opts.DoImport { + + } else { + + } + } else { + + } + } + +} \ No newline at end of file diff --git a/web/views/layouts/base.templ b/web/views/layouts/base.templ new file mode 100644 index 0000000..2efb800 --- /dev/null +++ b/web/views/layouts/base.templ @@ -0,0 +1,21 @@ +package layouts + +import "omnibill.net/omnibill/web/views/components" + +templ Base(pageHeading templ.Component) { + + + + + + + @components.LoadJSAsset("main.js", &components.ScriptAssetOptions{IsModule: true, IsDefer: true}) + if pageHeading != nil { + @pageHeading + } + + + { children... } + + +} \ No newline at end of file diff --git a/web/views/layouts/error.templ b/web/views/layouts/error.templ new file mode 100644 index 0000000..c732c20 --- /dev/null +++ b/web/views/layouts/error.templ @@ -0,0 +1,16 @@ +package layouts + +import "github.com/gofiber/fiber/v2" +import "strconv" +import "time" + +templ errorHeading(err fiber.Error) { + Error {err.Message} - OmniBill +} + +templ Error(err fiber.Error) { + @Base(errorHeading(err)) { +

{err.Message}

+

Status Code: {strconv.Itoa(err.Code)} | Time: {time.Now().Format("2006-1-2 15:4:5")}

+ } +} \ No newline at end of file