Initial Commit

This commit is contained in:
Shane C. 2024-11-03 15:33:08 -05:00
commit 33235d8f1e
Signed by: Shane C.
GPG key ID: E46B5FEA35B22FF9
35 changed files with 1960 additions and 0 deletions

24
.air.toml Normal file
View 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
View 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
View 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

Binary file not shown.

118
cmd/assets.go Normal file
View 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
View 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
View 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
View 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())
}
}

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

View file

@ -0,0 +1,11 @@
// Code generated by Omnibill - DO NOT EDIT
package handlers
{{ if ne (len .Imports) 0 }}
import (
{{- range .Imports }}
_ "{{.}}"
{{ end -}}
)
{{ end }}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
package utils

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

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

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