Initial Commit
This commit is contained in:
commit
33235d8f1e
35 changed files with 1960 additions and 0 deletions
24
.air.toml
Normal file
24
.air.toml
Normal file
|
@ -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
|
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
@ -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/
|
13
.gitlab-ci.yml
Normal file
13
.gitlab-ci.yml
Normal file
|
@ -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
|
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
118
cmd/assets.go
Normal file
118
cmd/assets.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
//go:build dev
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package cmd
|
||||||
|
Copyright © 2024 Shane C. <shane@scaffoe.com>
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
cmd/generate.go
Normal file
21
cmd/generate.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
//go:build dev
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package cmd
|
||||||
|
Copyright © 2024 Shane C. <shane@scaffoe.com>
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
355
cmd/handler.go
Normal file
355
cmd/handler.go
Normal file
|
@ -0,0 +1,355 @@
|
||||||
|
//go:build dev
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package cmd
|
||||||
|
Copyright © 2024 Shane C. <shane@scaffoe.com>
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
66
cmd/root.go
Normal file
66
cmd/root.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
Package cmd
|
||||||
|
Copyright © 2024 Shane C. <shane@scaffoe.com>
|
||||||
|
*/
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
81
cmd/templates/handler.go.tmpl
Normal file
81
cmd/templates/handler.go.tmpl
Normal file
|
@ -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 -}}
|
11
cmd/templates/imports.go.tmpl
Normal file
11
cmd/templates/imports.go.tmpl
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Code generated by Omnibill - DO NOT EDIT
|
||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
{{ if ne (len .Imports) 0 }}
|
||||||
|
import (
|
||||||
|
{{- range .Imports }}
|
||||||
|
_ "{{.}}"
|
||||||
|
{{ end -}}
|
||||||
|
)
|
||||||
|
{{ end }}
|
9
cmd/templates/view.go.tmpl
Normal file
9
cmd/templates/view.go.tmpl
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package {{.PackagePath}}
|
||||||
|
|
||||||
|
import "omnibill.net/omnibill/web/views/layouts"
|
||||||
|
|
||||||
|
templ Show() {
|
||||||
|
@layouts.Base(nil) {
|
||||||
|
<p>{{.Name}}</p>
|
||||||
|
}
|
||||||
|
}
|
33
config.example.toml
Normal file
33
config.example.toml
Normal file
|
@ -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"
|
96
eslint.config.ts
Normal file
96
eslint.config.ts
Normal file
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
75
go.mod
Normal file
75
go.mod
Normal file
|
@ -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
|
||||||
|
)
|
171
go.sum
Normal file
171
go.sum
Normal file
|
@ -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=
|
12
main.go
Normal file
12
main.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2024 Shane C. <shane@scaffoe.com>
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"omnibill.net/omnibill/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
80
models/models.go
Normal file
80
models/models.go
Normal file
|
@ -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"`
|
||||||
|
}
|
44
package.json
Normal file
44
package.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
8
postcss.config.ts
Normal file
8
postcss.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import type { Config } from 'postcss-load-config';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
} satisfies Config;
|
79
shared/hash.go
Normal file
79
shared/hash.go
Normal file
|
@ -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
|
||||||
|
|
||||||
|
}
|
36
shared/i18n.go
Normal file
36
shared/i18n.go
Normal file
|
@ -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
|
||||||
|
|
||||||
|
}
|
44
shared/json.go
Normal file
44
shared/json.go
Normal file
|
@ -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
|
||||||
|
}
|
23
shared/json_test.go
Normal file
23
shared/json_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
24
shared/postgres.go
Normal file
24
shared/postgres.go
Normal file
|
@ -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()
|
||||||
|
}
|
20
tailwind.config.ts
Normal file
20
tailwind.config.ts
Normal file
|
@ -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;
|
||||||
|
|
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
31
web/assets/css/styles.css
Normal file
31
web/assets/css/styles.css
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
50
web/middleware/auth.go
Normal file
50
web/middleware/auth.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
264
web/server.go
Normal file
264
web/server.go
Normal file
|
@ -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
|
||||||
|
}
|
33
web/utils/handlers.go
Normal file
33
web/utils/handlers.go
Normal file
|
@ -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
|
||||||
|
}
|
15
web/utils/render.go
Normal file
15
web/utils/render.go
Normal file
|
@ -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)
|
||||||
|
}
|
1
web/utils/session.go
Normal file
1
web/utils/session.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package utils
|
29
web/views/components/utils.templ
Normal file
29
web/views/components/utils.templ
Normal file
|
@ -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 {
|
||||||
|
<script type="module" src={"/assets/js/" + assetName}></script>
|
||||||
|
} else {
|
||||||
|
if opts.IsModule {
|
||||||
|
if opts.DoImport {
|
||||||
|
<script type="module">
|
||||||
|
{"import \"/assets/js/\"" + assetName}
|
||||||
|
</script>
|
||||||
|
} else {
|
||||||
|
<script type="module" src={"/assets/js/" + assetName} defer?={opts.IsDefer} async?={opts.IsAsync}></script>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
<script src={"/assets/js/" + assetName} defer?={opts.IsDefer} async?={opts.IsAsync}></script>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
web/views/layouts/base.templ
Normal file
21
web/views/layouts/base.templ
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package layouts
|
||||||
|
|
||||||
|
import "omnibill.net/omnibill/web/views/components"
|
||||||
|
|
||||||
|
templ Base(pageHeading templ.Component) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||||
|
@components.LoadJSAsset("main.js", &components.ScriptAssetOptions{IsModule: true, IsDefer: true})
|
||||||
|
if pageHeading != nil {
|
||||||
|
@pageHeading
|
||||||
|
}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{ children... }
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
16
web/views/layouts/error.templ
Normal file
16
web/views/layouts/error.templ
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package layouts
|
||||||
|
|
||||||
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
import "strconv"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
templ errorHeading(err fiber.Error) {
|
||||||
|
<title>Error {err.Message} - OmniBill</title>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Error(err fiber.Error) {
|
||||||
|
@Base(errorHeading(err)) {
|
||||||
|
<h1>{err.Message}</h1>
|
||||||
|
<h2>Status Code: {strconv.Itoa(err.Code)} | Time: {time.Now().Format("2006-1-2 15:4:5")}</h2>
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue