feat: initial commit
This commit is contained in:
commit
ee4a65b345
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
/data.slite*
|
||||
/bin
|
||||
/tmp
|
||||
/tools
|
20
.env.dist
Normal file
20
.env.dist
Normal file
@ -0,0 +1,20 @@
|
||||
# See internal/config for more configuration parameters
|
||||
|
||||
# Log level (here DEBUG)
|
||||
CLEARCASE_LOGGER_LEVEL=-4
|
||||
|
||||
# Gitea auth provider (disabled if empty)
|
||||
CLEARCASE_AUTH_PROVIDERS_GITEA_KEY=
|
||||
CLEARCASE_AUTH_PROVIDERS_GITEA_SECRET=
|
||||
CLEARCASE_AUTH_PROVIDERS_GITEA_SCOPES="user:email"
|
||||
|
||||
# HTTP session keys (list of 32 characters strings, should be modified in production)
|
||||
CLEARCASE_HTTP_SESSION_KEYS=abcdefghijklmnopqrstuvwxyz000000
|
||||
|
||||
# Base URL, used in templates and link generation
|
||||
CLEARCASE_HTTP_BASE_URL=http://localhost:3001
|
||||
|
||||
# LLM Provider
|
||||
# Example with ollama - llama3.1:8b :
|
||||
CLEARCASE_LLM_PROVIDER_BASE_URL="http://localhost:11434/api/"
|
||||
CLEARCASE_LLM_PROVIDER_MODEL="llama3.1:8b"
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/bin
|
||||
/tmp
|
||||
/tools
|
||||
*.db
|
||||
*.sqlite*
|
||||
/.env
|
27
.goreleaser.yaml
Normal file
27
.goreleaser.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- targets: [go_first_class]
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
dir: ./cmd/clearcase
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -w -s -X 'forge.cadoles.com/wpetit/clearcase/internal/build.ShortVersion={{ .Version }}' -X 'forge.cadoles.com/wpetit/clearcase/internal/build.LongVersion={{ .Version }}'
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
|
||||
release:
|
||||
name_template: "v{{ .Version }}"
|
54
Makefile
Normal file
54
Makefile
Normal file
@ -0,0 +1,54 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
GIT_SHORT_VERSION ?= $(shell git describe --tags --abbrev=8 --always)
|
||||
GIT_LONG_VERSION ?= $(shell git describe --tags --abbrev=8 --dirty --always --long)
|
||||
LDFLAGS ?= -w -s \
|
||||
-X 'forge.cadoles.com/wpetit/clearcase/internal/build.ShortVersion=$(GIT_SHORT_VERSION)' \
|
||||
-X 'forge.cadoles.com/wpetit/clearcase/internal/build.LongVersion=$(GIT_LONG_VERSION)'
|
||||
|
||||
GCFLAGS ?= -trimpath=$(PWD)
|
||||
ASMFLAGS ?= -trimpath=$(PWD) \
|
||||
|
||||
CI_EVENT ?= push
|
||||
|
||||
watch: tools/modd/bin/modd bin/templ
|
||||
tools/modd/bin/modd
|
||||
|
||||
run-with-env: .env
|
||||
( set -o allexport && source .env && set +o allexport && $(value CMD))
|
||||
|
||||
build: generate
|
||||
CGO_ENABLED=0 \
|
||||
go build \
|
||||
-ldflags "$(LDFLAGS)" \
|
||||
-gcflags "$(GCFLAGS)" \
|
||||
-asmflags "$(ASMFLAGS)" \
|
||||
-o ./bin/clearcase ./cmd/clearcase
|
||||
|
||||
generate: tools/templ/bin/templ
|
||||
tools/templ/bin/templ generate
|
||||
|
||||
bin/templ: tools/templ/bin/templ
|
||||
mkdir -p bin
|
||||
ln -fs $(PWD)/tools/templ/bin/templ bin/templ
|
||||
|
||||
tools/templ/bin/templ:
|
||||
mkdir -p tools/templ/bin
|
||||
GOBIN=$(PWD)/tools/templ/bin go install github.com/a-h/templ/cmd/templ@v0.3.819
|
||||
|
||||
tools/modd/bin/modd:
|
||||
mkdir -p tools/modd/bin
|
||||
GOBIN=$(PWD)/tools/modd/bin go install github.com/cortesi/modd/cmd/modd@latest
|
||||
|
||||
tools/act/bin/act:
|
||||
mkdir -p tools/act/bin
|
||||
cd tools/act && curl https://raw.githubusercontent.com/nektos/act/master/install.sh | bash -
|
||||
|
||||
ci: tools/act/bin/act
|
||||
tools/act/bin/act $(CI_EVENT)
|
||||
|
||||
.env:
|
||||
cp .env.dist .env
|
||||
|
||||
include misc/*/*.mk
|
||||
|
16
README.md
Normal file
16
README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Rkvst
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
# Generate .env from template
|
||||
make .env
|
||||
|
||||
# Update .env with your own values
|
||||
vim .env
|
||||
|
||||
# Start application
|
||||
make watch
|
||||
```
|
||||
|
||||
Then open http://localhost:3000 in your browser.
|
49
cmd/clearcase/main.go
Normal file
49
cmd/clearcase/main.go
Normal file
@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/config"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/setup"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
conf, err := config.Parse()
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "could not parse config", slog.Any("error", errors.WithStack(err)))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.SetLogLoggerLevel(slog.Level(conf.Logger.Level))
|
||||
|
||||
slog.DebugContext(ctx, "using configuration", slog.Any("config", conf))
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
slog.InfoContext(ctx, "use ctrl+c to interrupt")
|
||||
<-sig
|
||||
cancel()
|
||||
}()
|
||||
|
||||
server, err := setup.NewHTTPServerFromConfig(ctx, conf)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "could not setup http server", slog.Any("error", errors.WithStack(err)))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.InfoContext(ctx, "starting server", slog.Any("address", conf.HTTP.Address))
|
||||
|
||||
if err := server.Run(ctx); err != nil {
|
||||
slog.Error("could not run server", slog.Any("error", errors.WithStack(err)))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
29
go.mod
Normal file
29
go.mod
Normal file
@ -0,0 +1,29 @@
|
||||
module forge.cadoles.com/wpetit/clearcase
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.819
|
||||
github.com/caarlos0/env/v11 v11.2.2
|
||||
github.com/gabriel-vasile/mimetype v1.4.7
|
||||
github.com/gorilla/sessions v1.1.1
|
||||
github.com/markbates/goth v1.80.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/samber/slog-http v1.4.4
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.20.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/mux v1.6.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.17.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
)
|
80
go.sum
Normal file
80
go.sum
Normal file
@ -0,0 +1,80 @@
|
||||
cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
|
||||
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
github.com/a-h/templ v0.3.819 h1:KDJ5jTFN15FyJnmSmo2gNirIqt7hfvBD2VXVDTySckM=
|
||||
github.com/a-h/templ v0.3.819/go.mod h1:iDJKJktpttVKdWoTkRNNLcllRI+BlpopJc+8au3gOUo=
|
||||
github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg=
|
||||
github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
|
||||
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/samber/slog-http v1.4.4 h1:NuENLy39Lk6b7wfj9cG9R5C/JLZR4t6pb9cwlyroybI=
|
||||
github.com/samber/slog-http v1.4.4/go.mod h1:PAcQQrYFo5KM7Qbk50gNNwKEAMGCyfsw6GN5dI0iv9g=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
4
internal/build/version.go
Normal file
4
internal/build/version.go
Normal file
@ -0,0 +1,4 @@
|
||||
package build
|
||||
|
||||
var ShortVersion = "unknown"
|
||||
var LongVersion = "unknown"
|
30
internal/config/auth.go
Normal file
30
internal/config/auth.go
Normal file
@ -0,0 +1,30 @@
|
||||
package config
|
||||
|
||||
type Auth struct {
|
||||
DefaultAdmin DefaultAdmin `envPrefix:"DEFAULT_ADMIN_"`
|
||||
Providers AuthProviders `envPrefix:"PROVIDERS_"`
|
||||
}
|
||||
|
||||
type DefaultAdmin struct {
|
||||
Email string `env:"EMAIL,expand"`
|
||||
Provider string `env:"PROVIDER,expand"`
|
||||
}
|
||||
|
||||
type AuthProviders struct {
|
||||
Google OAuth2Provider `envPrefix:"GOOGLE_"`
|
||||
Github OAuth2Provider `envPrefix:"GITHUB_"`
|
||||
OIDC OIDCProvider `envPrefix:"OIDC_"`
|
||||
}
|
||||
|
||||
type OAuth2Provider struct {
|
||||
Key string `env:"KEY,expand"`
|
||||
Secret string `env:"SECRET,expand"`
|
||||
Scopes []string `env:"SCOPES",expand"`
|
||||
}
|
||||
|
||||
type OIDCProvider struct {
|
||||
OAuth2Provider
|
||||
DiscoveryURL string `env:"DISCOVERY_URL,expand"`
|
||||
Icon string `env:"ICON,expand" envDefault:"fa-passport"`
|
||||
Label string `env:"LABEL,expand" envDefault:"OpenID Connect"`
|
||||
}
|
25
internal/config/config.go
Normal file
25
internal/config/config.go
Normal file
@ -0,0 +1,25 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/caarlos0/env/v11"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Logger Logger `envPrefix:"LOGGER_"`
|
||||
Auth Auth `envPrefix:"AUTH_"`
|
||||
HTTP HTTP `envPrefix:"HTTP_"`
|
||||
Storage Storage `envPrefix:"STORAGE_"`
|
||||
LLM LLM `envPrefix:"LLM_"`
|
||||
}
|
||||
|
||||
func Parse() (*Config, error) {
|
||||
conf, err := env.ParseAsWithOptions[Config](env.Options{
|
||||
Prefix: "CLEARCASE_",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &conf, nil
|
||||
}
|
21
internal/config/http.go
Normal file
21
internal/config/http.go
Normal file
@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type HTTP struct {
|
||||
BaseURL string `env:"BASE_URL" envDefault:"http://localhost:3000"`
|
||||
Address string `env:"ADDRESS,expand" envDefault:":3000"`
|
||||
Session Session `envPrefix:"SESSION_"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Keys []string `env:"KEYS"`
|
||||
Cookie Cookie `envPrefix:"COOKIE_"`
|
||||
}
|
||||
|
||||
type Cookie struct {
|
||||
Path string `env:"PATH,expand" envDefault:"/"`
|
||||
HTTPOnly bool `env:"HTTP_ONLY,expand" envDefault:"true"`
|
||||
Secure bool `env:"SECURE,expand" envDefault:"false"`
|
||||
MaxAge time.Duration `env:"MAX_AGE,expand" envDefault:"24h"`
|
||||
}
|
12
internal/config/llm.go
Normal file
12
internal/config/llm.go
Normal file
@ -0,0 +1,12 @@
|
||||
package config
|
||||
|
||||
type LLM struct {
|
||||
Provider LLMProvider `envPrefix:"PROVIDER_"`
|
||||
}
|
||||
|
||||
type LLMProvider struct {
|
||||
Name string `env:"NAME" envDefault:"openai"`
|
||||
BaseURL string `env:"BASE_URL" envDefault:"https://api.openai.com/v1/"`
|
||||
Key string `env:"KEY"`
|
||||
Model string `env:"MODEL" envDefault:"gpt-4o-mini"`
|
||||
}
|
5
internal/config/logger.go
Normal file
5
internal/config/logger.go
Normal file
@ -0,0 +1,5 @@
|
||||
package config
|
||||
|
||||
type Logger struct {
|
||||
Level int `env:"LEVEL" envDefault:"0"`
|
||||
}
|
19
internal/config/storage.go
Normal file
19
internal/config/storage.go
Normal file
@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
type Storage struct {
|
||||
Database Database `envPrefix:"DATABASE_"`
|
||||
Object Object `envPrefix:"OBJECT_"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
DSN string `env:"DSN" envDefault:"sqlite://data.sqlite"`
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
DSN string `env:"DSN" envDefault:"sqlite://data.sqlite"`
|
||||
Encryption Encryption `envPrefix:"ENCRYPTION_"`
|
||||
}
|
||||
|
||||
type Encryption struct {
|
||||
Key string `env:"KEY,unset"`
|
||||
}
|
50
internal/core/workflow/error.go
Normal file
50
internal/core/workflow/error.go
Normal file
@ -0,0 +1,50 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CompensationError struct {
|
||||
executionErr error
|
||||
compensationErrs []error
|
||||
}
|
||||
|
||||
func (e *CompensationError) ExecutionError() error {
|
||||
return e.executionErr
|
||||
}
|
||||
|
||||
func (e *CompensationError) CompensationErrors() []error {
|
||||
return e.compensationErrs
|
||||
}
|
||||
|
||||
func (e *CompensationError) Error() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("compensation error: ")
|
||||
sb.WriteString("execution error '")
|
||||
sb.WriteString(e.ExecutionError().Error())
|
||||
sb.WriteString("' resulted in following compensation errors: ")
|
||||
|
||||
for idx, err := range e.CompensationErrors() {
|
||||
if idx > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
|
||||
sb.WriteString("[")
|
||||
sb.WriteString(strconv.FormatInt(int64(idx), 10))
|
||||
sb.WriteString("] ")
|
||||
sb.WriteString(err.Error())
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func NewCompensationError(executionErr error, compensationErrs ...error) *CompensationError {
|
||||
return &CompensationError{
|
||||
executionErr: executionErr,
|
||||
compensationErrs: compensationErrs,
|
||||
}
|
||||
}
|
||||
|
||||
var _ error = &CompensationError{}
|
40
internal/core/workflow/step.go
Normal file
40
internal/core/workflow/step.go
Normal file
@ -0,0 +1,40 @@
|
||||
package workflow
|
||||
|
||||
import "context"
|
||||
|
||||
type Step interface {
|
||||
Execute(ctx context.Context) error
|
||||
Compensate(ctx context.Context) error
|
||||
}
|
||||
|
||||
type step struct {
|
||||
execute func(ctx context.Context) error
|
||||
compensate func(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Compensate implements Step.
|
||||
func (s *step) Compensate(ctx context.Context) error {
|
||||
if s.compensate == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.compensate(ctx)
|
||||
}
|
||||
|
||||
// Execute implements Step.
|
||||
func (s *step) Execute(ctx context.Context) error {
|
||||
if s.execute == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.execute(ctx)
|
||||
}
|
||||
|
||||
var _ Step = &step{}
|
||||
|
||||
func StepFunc(execute func(ctx context.Context) error, compensate func(ctx context.Context) error) Step {
|
||||
return &step{
|
||||
execute: execute,
|
||||
compensate: compensate,
|
||||
}
|
||||
}
|
46
internal/core/workflow/workflow.go
Normal file
46
internal/core/workflow/workflow.go
Normal file
@ -0,0 +1,46 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Workflow struct {
|
||||
steps []Step
|
||||
}
|
||||
|
||||
func (w *Workflow) Execute(ctx context.Context) error {
|
||||
for idx, step := range w.steps {
|
||||
if executionErr := step.Execute(ctx); executionErr != nil {
|
||||
if compensationErrs := w.compensate(ctx, idx-1); compensationErrs != nil {
|
||||
return errors.WithStack(NewCompensationError(executionErr, compensationErrs...))
|
||||
}
|
||||
|
||||
return errors.WithStack(executionErr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Workflow) compensate(ctx context.Context, fromIndex int) []error {
|
||||
errs := make([]error, 0)
|
||||
for idx := fromIndex; idx >= 0; idx -= 1 {
|
||||
act := w.steps[idx]
|
||||
|
||||
if err := act.Compensate(ctx); err != nil {
|
||||
errs = append(errs, errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(steps ...Step) *Workflow {
|
||||
return &Workflow{steps: steps}
|
||||
}
|
22
internal/crypto/rand.go
Normal file
22
internal/crypto/rand.go
Normal file
@ -0,0 +1,22 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func RandomBytes(size int) ([]byte, error) {
|
||||
data := make([]byte, size)
|
||||
|
||||
read, err := rand.Read(data)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if read != size {
|
||||
return nil, errors.New("unexpected number of read bytes")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
28
internal/http/context/base_url.go
Normal file
28
internal/http/context/base_url.go
Normal file
@ -0,0 +1,28 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const keyBaseURL = "baseURL"
|
||||
|
||||
func BaseURL(ctx context.Context) *url.URL {
|
||||
rawBaseURL, ok := ctx.Value(keyBaseURL).(string)
|
||||
if !ok {
|
||||
panic(errors.New("no base url in context"))
|
||||
}
|
||||
|
||||
baseURL, err := url.Parse(rawBaseURL)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return baseURL
|
||||
}
|
||||
|
||||
func SetBaseURL(ctx context.Context, baseURL string) context.Context {
|
||||
return context.WithValue(ctx, keyBaseURL, baseURL)
|
||||
}
|
3
internal/http/context/context.go
Normal file
3
internal/http/context/context.go
Normal file
@ -0,0 +1,3 @@
|
||||
package context
|
||||
|
||||
type key string
|
23
internal/http/context/current_url.go
Normal file
23
internal/http/context/current_url.go
Normal file
@ -0,0 +1,23 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const keyCurrentURL = "currentURL"
|
||||
|
||||
func CurrentURL(ctx context.Context) *url.URL {
|
||||
currentURL, ok := ctx.Value(keyCurrentURL).(*url.URL)
|
||||
if !ok {
|
||||
panic(errors.New("no current url in context"))
|
||||
}
|
||||
|
||||
return currentURL
|
||||
}
|
||||
|
||||
func SetCurrentURL(ctx context.Context, u *url.URL) context.Context {
|
||||
return context.WithValue(ctx, keyCurrentURL, u)
|
||||
}
|
23
internal/http/context/user.go
Normal file
23
internal/http/context/user.go
Normal file
@ -0,0 +1,23 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const keyUser = "user"
|
||||
|
||||
func User(ctx context.Context) *goth.User {
|
||||
user, ok := ctx.Value(keyUser).(*goth.User)
|
||||
if !ok {
|
||||
panic(errors.New("no user in context"))
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func SetUser(ctx context.Context, user *goth.User) context.Context {
|
||||
return context.WithValue(ctx, keyUser, user)
|
||||
}
|
9
internal/http/form/error.go
Normal file
9
internal/http/form/error.go
Normal file
@ -0,0 +1,9 @@
|
||||
package form
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrFieldNotFound = errors.New("field not found")
|
||||
ErrUnexpectedValue = errors.New("unexpected value")
|
||||
ErrMissingValue = errors.New("missing value")
|
||||
)
|
85
internal/http/form/field.go
Normal file
85
internal/http/form/field.go
Normal file
@ -0,0 +1,85 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Attrs map[string]any
|
||||
|
||||
type FieldUnmarshaller interface {
|
||||
Unmarshal(v string) error
|
||||
}
|
||||
|
||||
type ValidationFunc func(f *Field) error
|
||||
|
||||
type Field struct {
|
||||
name string
|
||||
attrs map[string]any
|
||||
validators []ValidationFunc
|
||||
}
|
||||
|
||||
func (f *Field) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *Field) Get(key string) (value any, exists bool) {
|
||||
value, exists = f.attrs[key]
|
||||
return
|
||||
}
|
||||
|
||||
func (f *Field) Set(key string, value any) {
|
||||
f.attrs[key] = value
|
||||
}
|
||||
|
||||
func (f *Field) Del(key string) {
|
||||
delete(f.attrs, key)
|
||||
}
|
||||
|
||||
func (f *Field) Attrs() map[string]any {
|
||||
return f.attrs
|
||||
}
|
||||
|
||||
func (f *Field) Validate() error {
|
||||
for _, fn := range f.validators {
|
||||
if err := fn(f); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewField(name string, attrs Attrs, validators ...ValidationFunc) *Field {
|
||||
return &Field{
|
||||
name: name,
|
||||
attrs: attrs,
|
||||
validators: validators,
|
||||
}
|
||||
}
|
||||
|
||||
func FormFieldAttr[T any](f *Form, name string, attr string) (T, error) {
|
||||
field := f.Field(name)
|
||||
if field == nil {
|
||||
return *new(T), errors.WithStack(ErrFieldNotFound)
|
||||
}
|
||||
|
||||
value, err := FieldAttr[T](field, attr)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func FieldAttr[T any](f *Field, name string) (T, error) {
|
||||
raw, ok := f.Get(name)
|
||||
if !ok {
|
||||
return *new(T), errors.WithStack(ErrMissingValue)
|
||||
}
|
||||
|
||||
value, ok := raw.(T)
|
||||
if !ok {
|
||||
return *new(T), errors.WithStack(ErrUnexpectedValue)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
131
internal/http/form/form.go
Normal file
131
internal/http/form/form.go
Normal file
@ -0,0 +1,131 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ValidationErrors map[string]error
|
||||
|
||||
type Form struct {
|
||||
fields []*Field
|
||||
errors ValidationErrors
|
||||
}
|
||||
|
||||
func (f *Form) Handle(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := f.Parse(r.Form); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) HandleMultipart(r *http.Request, maxMemory int64) error {
|
||||
if err := r.ParseMultipartForm(maxMemory); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := f.ParseMultipart(r.MultipartForm); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) ParseMultipart(multi *multipart.Form) error {
|
||||
for _, f := range f.fields {
|
||||
name := f.Name()
|
||||
|
||||
values, exists := multi.Value[name]
|
||||
if !exists {
|
||||
f.Del("value")
|
||||
} else {
|
||||
f.Set("value", values)
|
||||
}
|
||||
|
||||
files, exists := multi.File[name]
|
||||
if !exists {
|
||||
f.Del("file")
|
||||
} else {
|
||||
f.Set("file", files)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) Parse(values url.Values) error {
|
||||
for _, f := range f.fields {
|
||||
name := f.Name()
|
||||
if !values.Has(name) {
|
||||
f.Del("value")
|
||||
continue
|
||||
}
|
||||
|
||||
value := values.Get(name)
|
||||
f.Set("value", value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) Validate() ValidationErrors {
|
||||
var errs ValidationErrors
|
||||
|
||||
for _, f := range f.fields {
|
||||
if err := f.Validate(); err != nil {
|
||||
if errs == nil {
|
||||
errs = make(ValidationErrors)
|
||||
}
|
||||
|
||||
errs[f.Name()] = err
|
||||
}
|
||||
}
|
||||
|
||||
f.errors = errs
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (f *Form) Field(name string) *Field {
|
||||
for _, field := range f.fields {
|
||||
if field.Name() == name {
|
||||
return field
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Form) Error(name string) (ValidationError, bool) {
|
||||
err, exists := f.errors[name]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var validationErr ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
return validationErr, true
|
||||
}
|
||||
|
||||
return NewValidationError("Unexpected error"), true
|
||||
}
|
||||
|
||||
func (f *Form) Extend(fields ...*Field) *Form {
|
||||
fields = append(f.fields, fields...)
|
||||
return New(fields...)
|
||||
}
|
||||
|
||||
func New(fields ...*Field) *Form {
|
||||
return &Form{
|
||||
fields: fields,
|
||||
errors: make(ValidationErrors),
|
||||
}
|
||||
}
|
27
internal/http/form/match_regexp.go
Normal file
27
internal/http/form/match_regexp.go
Normal file
@ -0,0 +1,27 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func MatchRegExp(pattern string, message string) func(f *Field) error {
|
||||
return func(f *Field) error {
|
||||
value, err := FieldAttr[string](f, "value")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
matches, err := regexp.MatchString(pattern, value)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return NewValidationError(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
22
internal/http/form/non_empty.go
Normal file
22
internal/http/form/non_empty.go
Normal file
@ -0,0 +1,22 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NonEmpty(message string) func(f *Field) error {
|
||||
return func(f *Field) error {
|
||||
value, err := FieldAttr[string](f, "value")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return NewValidationError(message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
26
internal/http/form/validation.go
Normal file
26
internal/http/form/validation.go
Normal file
@ -0,0 +1,26 @@
|
||||
package form
|
||||
|
||||
type ValidationError interface {
|
||||
error
|
||||
Message() string
|
||||
}
|
||||
|
||||
type validationError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
// Error implements ValidationError.
|
||||
func (v *validationError) Error() string {
|
||||
return "validation error"
|
||||
}
|
||||
|
||||
// Message implements ValidationError.
|
||||
func (v *validationError) Message() string {
|
||||
return v.message
|
||||
}
|
||||
|
||||
var _ ValidationError = &validationError{}
|
||||
|
||||
func NewValidationError(message string) ValidationError {
|
||||
return &validationError{message: message}
|
||||
}
|
34
internal/http/handler/webui/auth/component/login_page.templ
Normal file
34
internal/http/handler/webui/auth/component/login_page.templ
Normal file
@ -0,0 +1,34 @@
|
||||
package component
|
||||
|
||||
import common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||
|
||||
type LoginPageVModel struct {
|
||||
Providers []ProviderVModel
|
||||
}
|
||||
|
||||
type ProviderVModel struct {
|
||||
ID string
|
||||
Label string
|
||||
Icon string
|
||||
}
|
||||
|
||||
templ LoginPage(vmodel LoginPageVModel) {
|
||||
@common.Page() {
|
||||
<div class="is-flex is-justify-content-center is-align-items-center is-fullheight">
|
||||
<nav class="panel is-link" style="min-width: 33%">
|
||||
<p class="panel-heading">
|
||||
<div class="title">ClearCase</div>
|
||||
<span> - choose your provider</span>
|
||||
</p>
|
||||
for _, provider := range vmodel.Providers {
|
||||
<a class="panel-block" href={ templ.URL("/auth/providers/" + provider.ID) } hx-boost="false">
|
||||
<span class="panel-icon is-size-3">
|
||||
<i class={ "fab", provider.Icon } aria-hidden="true"></i>
|
||||
</span>
|
||||
{ provider.Label }
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
124
internal/http/handler/webui/auth/component/login_page_templ.go
Normal file
124
internal/http/handler/webui/auth/component/login_page_templ.go
Normal file
@ -0,0 +1,124 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package component
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||
|
||||
type LoginPageVModel struct {
|
||||
Providers []ProviderVModel
|
||||
}
|
||||
|
||||
type ProviderVModel struct {
|
||||
ID string
|
||||
Label string
|
||||
Icon string
|
||||
}
|
||||
|
||||
func LoginPage(vmodel LoginPageVModel) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"is-flex is-justify-content-center is-align-items-center is-fullheight\"><nav class=\"panel is-link\" style=\"min-width: 33%\"><p class=\"panel-heading\"><div class=\"title\">ClearCase</div><span> - choose your provider</span></p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, provider := range vmodel.Providers {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a class=\"panel-block\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL = templ.URL("/auth/providers/" + provider.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-boost=\"false\"><span class=\"panel-icon is-size-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 = []any{"fab", provider.Icon}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<i class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/auth/component/login_page.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" aria-hidden=\"true\"></i></span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(provider.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/auth/component/login_page.templ`, Line: 28, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</nav></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = common.Page().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
55
internal/http/handler/webui/auth/handler.go
Normal file
55
internal/http/handler/webui/auth/handler.go
Normal file
@ -0,0 +1,55 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/auth/component"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
type Provider = component.ProviderVModel
|
||||
|
||||
type Handler struct {
|
||||
mux *http.ServeMux
|
||||
sessionStore sessions.Store
|
||||
sessionName string
|
||||
providers []Provider
|
||||
defaultAdmin *DefaultAdmin
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func NewHandler(sessionStore sessions.Store, funcs ...OptionFunc) *Handler {
|
||||
opts := NewOptions(funcs...)
|
||||
h := &Handler{
|
||||
mux: http.NewServeMux(),
|
||||
sessionStore: sessionStore,
|
||||
sessionName: opts.SessionName,
|
||||
providers: opts.Providers,
|
||||
defaultAdmin: opts.DefaultAdmin,
|
||||
}
|
||||
|
||||
h.mux.HandleFunc("GET /login", h.getLoginPage)
|
||||
h.mux.Handle("GET /providers/{provider}", withContextProvider(http.HandlerFunc(h.handleProvider)))
|
||||
h.mux.Handle("GET /providers/{provider}/callback", withContextProvider(http.HandlerFunc(h.handleProviderCallback)))
|
||||
h.mux.HandleFunc("GET /logout", h.handleLogout)
|
||||
h.mux.Handle("GET /providers/{provider}/logout", withContextProvider(http.HandlerFunc(h.handleProviderLogout)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
var _ http.Handler = &Handler{}
|
||||
|
||||
func withContextProvider(h http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.PathValue("provider")
|
||||
r = r.WithContext(context.WithValue(r.Context(), "provider", provider))
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
16
internal/http/handler/webui/auth/login_page.go
Normal file
16
internal/http/handler/webui/auth/login_page.go
Normal file
@ -0,0 +1,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/auth/component"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
func (h *Handler) getLoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
vmodel := component.LoginPageVModel{
|
||||
Providers: h.providers,
|
||||
}
|
||||
login := component.LoginPage(vmodel)
|
||||
templ.Handler(login).ServeHTTP(w, r)
|
||||
}
|
33
internal/http/handler/webui/auth/middleware.go
Normal file
33
internal/http/handler/webui/auth/middleware.go
Normal file
@ -0,0 +1,33 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/context"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrUserNotFound = errors.New("user not found")
|
||||
|
||||
func (h *Handler) Middleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.retrieveSessionUser(r)
|
||||
if err != nil {
|
||||
slog.ErrorContext(r.Context(), "could not retrieve user from session", slog.Any("error", errors.WithStack(err)))
|
||||
http.Redirect(w, r, "/auth/login", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.SetUser(ctx, user)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
45
internal/http/handler/webui/auth/options.go
Normal file
45
internal/http/handler/webui/auth/options.go
Normal file
@ -0,0 +1,45 @@
|
||||
package auth
|
||||
|
||||
type DefaultAdmin struct {
|
||||
Provider string
|
||||
Email string
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Providers []Provider
|
||||
DefaultAdmin *DefaultAdmin
|
||||
SessionName string
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
Providers: make([]Provider, 0),
|
||||
SessionName: "rkvst_auth",
|
||||
}
|
||||
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithProviders(providers ...Provider) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Providers = providers
|
||||
}
|
||||
}
|
||||
|
||||
func WithDefaultAdmin(defaultAdmin DefaultAdmin) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.DefaultAdmin = &defaultAdmin
|
||||
}
|
||||
}
|
||||
|
||||
func WithSessionName(sessionName string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.SessionName = sessionName
|
||||
}
|
||||
}
|
71
internal/http/handler/webui/auth/provider.go
Normal file
71
internal/http/handler/webui/auth/provider.go
Normal file
@ -0,0 +1,71 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (h *Handler) handleProvider(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := gothic.CompleteUserAuth(w, r); err == nil {
|
||||
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
gothic.BeginAuthHandler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleProviderCallback(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := gothic.CompleteUserAuth(w, r)
|
||||
if err != nil {
|
||||
slog.ErrorContext(r.Context(), "could not complete user auth", slog.Any("error", errors.WithStack(err)))
|
||||
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
slog.DebugContext(ctx, "authenticated user", slog.Any("user", user))
|
||||
|
||||
if err := h.storeSessionUser(w, r, &user); err != nil {
|
||||
slog.ErrorContext(r.Context(), "could not store session user", slog.Any("error", errors.WithStack(err)))
|
||||
http.Redirect(w, r, "/auth/logout", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.retrieveSessionUser(r)
|
||||
if err != nil && !errors.Is(err, errSessionNotFound) {
|
||||
common.HandleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.clearSession(w, r); err != nil && !errors.Is(err, errSessionNotFound) {
|
||||
common.HandleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL := fmt.Sprintf("/auth/providers/%s/logout", user.Provider)
|
||||
|
||||
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (h *Handler) handleProviderLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if err := gothic.Logout(w, r); err != nil {
|
||||
common.HandleError(w, r, errors.WithStack(err))
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
73
internal/http/handler/webui/auth/session.go
Normal file
73
internal/http/handler/webui/auth/session.go
Normal file
@ -0,0 +1,73 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const userAttr = "u"
|
||||
const tenantIDAttr = "t"
|
||||
|
||||
var errSessionNotFound = errors.New("session not found")
|
||||
|
||||
func (h *Handler) storeSessionUser(w http.ResponseWriter, r *http.Request, user *goth.User) error {
|
||||
sess, err := h.getSession(r)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
sess.Values[userAttr] = user
|
||||
|
||||
if err := sess.Save(r, w); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) retrieveSessionUser(r *http.Request) (*goth.User, error) {
|
||||
sess, err := h.getSession(r)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
user, ok := sess.Values[userAttr].(*goth.User)
|
||||
if !ok {
|
||||
return nil, errors.WithStack(errSessionNotFound)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *Handler) getSession(r *http.Request) (*sessions.Session, error) {
|
||||
sess, err := h.sessionStore.Get(r, h.sessionName)
|
||||
if err != nil {
|
||||
slog.ErrorContext(r.Context(), "could not retrieve session from store", slog.Any("error", errors.WithStack(err)))
|
||||
return sess, errors.WithStack(errSessionNotFound)
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
func (h *Handler) clearSession(w http.ResponseWriter, r *http.Request) error {
|
||||
sess, err := h.getSession(r)
|
||||
if err != nil && !errors.Is(err, errSessionNotFound) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if sess == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sess.Options.MaxAge = -1
|
||||
|
||||
if err := sess.Save(r, w); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
3
internal/http/handler/webui/common/assets/bulma.min.css
vendored
Normal file
3
internal/http/handler/webui/common/assets/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
internal/http/handler/webui/common/assets/font-awesome.min.css
vendored
Normal file
9
internal/http/handler/webui/common/assets/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,165 @@
|
||||
Fonticons, Inc. (https://fontawesome.com)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Font Awesome Free License
|
||||
|
||||
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||
commercial projects, open source projects, or really almost whatever you want.
|
||||
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
The Font Awesome Free download is licensed under a Creative Commons
|
||||
Attribution 4.0 International License and applies to all icons packaged
|
||||
as SVG and JS file types.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Fonts: SIL OFL 1.1 License
|
||||
|
||||
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||
packaged as web and desktop font files.
|
||||
|
||||
Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
|
||||
with Reserved Font Name: "Font Awesome".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
SIL OPEN FONT LICENSE
|
||||
Version 1.1 - 26 February 2007
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting — in part or in whole — any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||
non-icon files.
|
||||
|
||||
Copyright 2024 Fonticons, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Attribution
|
||||
|
||||
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||
Awesome Free files already contain embedded comments with sufficient
|
||||
attribution, so you shouldn't need to do anything additional when using these
|
||||
files normally.
|
||||
|
||||
We've kept attribution comments terse, so we ask that you do not actively work
|
||||
to remove them from files, especially code. They're a great way for folks to
|
||||
learn about Font Awesome.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Brand Icons
|
||||
|
||||
All brand icons are trademarks of their respective owners. The use of these
|
||||
trademarks does not indicate endorsement of the trademark holder by Font
|
||||
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||
to represent the company, product, or service to which they refer.**
|
9
internal/http/handler/webui/common/assets/fontawesome/css/all.min.css
vendored
Normal file
9
internal/http/handler/webui/common/assets/fontawesome/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
internal/http/handler/webui/common/assets/htmx.min.js
vendored
Normal file
1
internal/http/handler/webui/common/assets/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
internal/http/handler/webui/common/assets/style.css
Normal file
18
internal/http/handler/webui/common/assets/style.css
Normal file
@ -0,0 +1,18 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-fullwidth {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.is-fullheight {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.has-text-ellipsis {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package component
|
||||
|
||||
type ErrorPageVModel struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
templ ErrorPage(vmodel ErrorPageVModel) {
|
||||
@Page(WithTitle("Error")) {
|
||||
<div class="is-flex is-justify-content-center is-align-items-center is-fullheight">
|
||||
<article class="message is-danger">
|
||||
<div class="message-header">
|
||||
<p>Error</p>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
{ vmodel.Message }
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package component
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
type ErrorPageVModel struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func ErrorPage(vmodel ErrorPageVModel) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"is-flex is-justify-content-center is-align-items-center is-fullheight\"><article class=\"message is-danger\"><div class=\"message-header\"><p>Error</p></div><div class=\"message-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(vmodel.Message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/error_page.templ`, Line: 15, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></article></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = Page(WithTitle("Error")).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
94
internal/http/handler/webui/common/component/field.templ
Normal file
94
internal/http/handler/webui/common/component/field.templ
Normal file
@ -0,0 +1,94 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/form"
|
||||
)
|
||||
|
||||
templ FormField(form *form.Form, id string, name string, label string) {
|
||||
{{ field := form.Field(name) }}
|
||||
if field != nil {
|
||||
<div class="field">
|
||||
{{ err, hasErr := form.Error(name) }}
|
||||
<label class={ "label", templ.KV("has-text-danger", hasErr) } for={ id }>{ label }</label>
|
||||
<div class="control">
|
||||
<input class={ "input", templ.KV("is-danger", hasErr) } id={ id } name={ field.Name() } { field.Attrs()... }/>
|
||||
if hasErr {
|
||||
<p class="help is-danger">{ err.Message() }</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ FormTextarea(form *form.Form, id string, name string, label string) {
|
||||
{{ field := form.Field(name) }}
|
||||
if field != nil {
|
||||
<div class="field">
|
||||
<label class="label" for={ id }>{ label }</label>
|
||||
<div class="control">
|
||||
{{ err, hasErr := form.Error(name) }}
|
||||
{{ value, hasValue := field.Get("value") }}
|
||||
<textarea class={ "textarea", templ.KV("is-danger", hasErr) } id={ id } name={ field.Name() } { field.Attrs()... }>
|
||||
if hasValue {
|
||||
{ value.(string) }
|
||||
}
|
||||
</textarea>
|
||||
if hasErr {
|
||||
<p class="help is-danger">{ err.Message() }</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ FormSelect(form *form.Form, id string, name string, label string, kvOptions ...string) {
|
||||
{{ field := form.Field(name) }}
|
||||
if field != nil {
|
||||
<div class="field">
|
||||
<label class="label" for={ id }>{ label }</label>
|
||||
<div class="control">
|
||||
{{ err, hasErr := form.Error(name) }}
|
||||
<div class="select is-fullwidth">
|
||||
<select id={ id } name={ field.Name() } { field.Attrs()... }>
|
||||
{{ options := keyValuesToOptions(kvOptions) }}
|
||||
for _, o := range options {
|
||||
<option value={ o.Value }>{ o.Label }</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
if hasErr {
|
||||
<p class="help is-danger">{ err.Message() }</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
type SelectOption struct {
|
||||
Value string
|
||||
Label string
|
||||
}
|
||||
|
||||
func keyValuesToOptions(kv []string) []SelectOption {
|
||||
if len(kv)%2 != 0 {
|
||||
panic(errors.New("expected pair number of key/values"))
|
||||
}
|
||||
|
||||
options := make([]SelectOption, 0)
|
||||
|
||||
var key string
|
||||
for idx := range kv {
|
||||
if idx%2 == 0 {
|
||||
key = kv[idx]
|
||||
continue
|
||||
}
|
||||
|
||||
options = append(options, SelectOption{
|
||||
Value: kv[idx],
|
||||
Label: key,
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
515
internal/http/handler/webui/common/component/field_templ.go
Normal file
515
internal/http/handler/webui/common/component/field_templ.go
Normal file
@ -0,0 +1,515 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package component
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/form"
|
||||
)
|
||||
|
||||
func FormField(form *form.Form, id string, name string, label string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
field := form.Field(name)
|
||||
if field != nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"field\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
err, hasErr := form.Error(name)
|
||||
var templ_7745c5c3_Var2 = []any{"label", templ.KV("has-text-danger", hasErr)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<label class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 13, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 13, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</label><div class=\"control\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 = []any{"input", templ.KV("is-danger", hasErr)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<input class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var6).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 15, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 15, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, field.Attrs())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if hasErr {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p class=\"help is-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(err.Message())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 17, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func FormTextarea(form *form.Form, id string, name string, label string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var11 == nil {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
field := form.Field(name)
|
||||
if field != nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"field\"><label class=\"label\" for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 28, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 28, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</label><div class=\"control\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
err, hasErr := form.Error(name)
|
||||
value, hasValue := field.Get("value")
|
||||
var templ_7745c5c3_Var14 = []any{"textarea", templ.KV("is-danger", hasErr)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<textarea class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 32, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 32, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, field.Attrs())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if hasValue {
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(value.(string))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 34, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</textarea> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if hasErr {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<p class=\"help is-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(err.Message())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 38, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func FormSelect(form *form.Form, id string, name string, label string, kvOptions ...string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var20 == nil {
|
||||
templ_7745c5c3_Var20 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
field := form.Field(name)
|
||||
if field != nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"field\"><label class=\"label\" for=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 49, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 49, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</label><div class=\"control\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
err, hasErr := form.Error(name)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"select is-fullwidth\"><select id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 53, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" name=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(field.Name())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 53, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, field.Attrs())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, ">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
options := keyValuesToOptions(kvOptions)
|
||||
for _, o := range options {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(o.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 56, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(o.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 56, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</select></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if hasErr {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<p class=\"help is-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(err.Message())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/field.templ`, Line: 61, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type SelectOption struct {
|
||||
Value string
|
||||
Label string
|
||||
}
|
||||
|
||||
func keyValuesToOptions(kv []string) []SelectOption {
|
||||
if len(kv)%2 != 0 {
|
||||
panic(errors.New("expected pair number of key/values"))
|
||||
}
|
||||
|
||||
options := make([]SelectOption, 0)
|
||||
|
||||
var key string
|
||||
for idx := range kv {
|
||||
if idx%2 == 0 {
|
||||
key = kv[idx]
|
||||
continue
|
||||
}
|
||||
|
||||
options = append(options, SelectOption{
|
||||
Value: kv[idx],
|
||||
Label: key,
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
68
internal/http/handler/webui/common/component/page.templ
Normal file
68
internal/http/handler/webui/common/component/page.templ
Normal file
@ -0,0 +1,68 @@
|
||||
package component
|
||||
|
||||
type PageOptions struct {
|
||||
Title string
|
||||
Head func() templ.Component
|
||||
}
|
||||
|
||||
type PageOptionFunc func(opts *PageOptions)
|
||||
|
||||
func WithTitle(title string) PageOptionFunc {
|
||||
return func(opts *PageOptions) {
|
||||
if title != "" {
|
||||
opts.Title = title + " | Rkvst"
|
||||
} else {
|
||||
opts.Title = "Rkvst"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithHead(fn func() templ.Component) PageOptionFunc {
|
||||
return func(opts *PageOptions) {
|
||||
opts.Head = fn
|
||||
}
|
||||
}
|
||||
|
||||
func NewPageOptions(funcs ...PageOptionFunc) *PageOptions {
|
||||
opts := &PageOptions{
|
||||
Title: "",
|
||||
Head: nil,
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
templ Page(funcs ...PageOptionFunc) {
|
||||
{{ opts := NewPageOptions(funcs...) }}
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{ opts.Title }</title>
|
||||
<link rel="icon" type="image/x-icon" href={ string(BaseURL(ctx, WithPath("/assets/favicon.ico"))) }/>
|
||||
<link rel="stylesheet" href={ string(BaseURL(ctx, WithPath("/assets/bulma.min.css"))) }/>
|
||||
<link rel="stylesheet" href={ string(BaseURL(ctx, WithPath("/assets/fontawesome/css/all.min.css"))) }/>
|
||||
<link rel="stylesheet" href={ string(BaseURL(ctx, WithPath("/assets/style.css"))) }/>
|
||||
<script src={ string(BaseURL(ctx, WithPath("/assets/htmx.min.js"))) }></script>
|
||||
if opts.Head != nil {
|
||||
@opts.Head()
|
||||
}
|
||||
</head>
|
||||
<body hx-boost="true" class="is-fullheight">
|
||||
<div id="main" class="is-fullheight">
|
||||
{ children... }
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
htmx.config.responseHandling = [
|
||||
{code:"204", swap: false}, // 204 - No Content by default does nothing, but is not an error
|
||||
{code:"[23]..", swap: true}, // 200 & 300 responses are non-errors and are swapped
|
||||
{code:"[45]..", swap: true, error:true}, // 400 & 500 responses are not swapped and are errors
|
||||
];
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
172
internal/http/handler/webui/common/component/page_templ.go
Normal file
172
internal/http/handler/webui/common/component/page_templ.go
Normal file
@ -0,0 +1,172 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package component
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
type PageOptions struct {
|
||||
Title string
|
||||
Head func() templ.Component
|
||||
}
|
||||
|
||||
type PageOptionFunc func(opts *PageOptions)
|
||||
|
||||
func WithTitle(title string) PageOptionFunc {
|
||||
return func(opts *PageOptions) {
|
||||
if title != "" {
|
||||
opts.Title = title + " | Rkvst"
|
||||
} else {
|
||||
opts.Title = "Rkvst"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithHead(fn func() templ.Component) PageOptionFunc {
|
||||
return func(opts *PageOptions) {
|
||||
opts.Head = fn
|
||||
}
|
||||
}
|
||||
|
||||
func NewPageOptions(funcs ...PageOptionFunc) *PageOptions {
|
||||
opts := &PageOptions{
|
||||
Title: "",
|
||||
Head: nil,
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func Page(funcs ...PageOptionFunc) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
opts := NewPageOptions(funcs...)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html data-theme=\"dark\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(opts.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/page.templ`, Line: 45, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"icon\" type=\"image/x-icon\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(string(BaseURL(ctx, WithPath("/assets/favicon.ico"))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/page.templ`, Line: 46, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><link rel=\"stylesheet\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(string(BaseURL(ctx, WithPath("/assets/bulma.min.css"))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/page.templ`, Line: 47, Col: 88}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><link rel=\"stylesheet\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(string(BaseURL(ctx, WithPath("/assets/fontawesome/css/all.min.css"))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/page.templ`, Line: 48, Col: 102}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><link rel=\"stylesheet\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(string(BaseURL(ctx, WithPath("/assets/style.css"))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/page.templ`, Line: 49, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"><script src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(string(BaseURL(ctx, WithPath("/assets/htmx.min.js"))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/common/component/page.templ`, Line: 50, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"></script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if opts.Head != nil {
|
||||
templ_7745c5c3_Err = opts.Head().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</head><body hx-boost=\"true\" class=\"is-fullheight\"><div id=\"main\" class=\"is-fullheight\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><script type=\"text/javascript\">\n\t\t\t\thtmx.config.responseHandling = [\n\t\t\t\t\t{code:\"204\", swap: false}, // 204 - No Content by default does nothing, but is not an error\n\t\t\t\t\t{code:\"[23]..\", swap: true}, // 200 & 300 responses are non-errors and are swapped\n\t\t\t\t\t{code:\"[45]..\", swap: true, error:true}, // 400 & 500 responses are not swapped and are errors\n\t\t\t\t];\n\t\t\t</script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
38
internal/http/handler/webui/common/component/util.go
Normal file
38
internal/http/handler/webui/common/component/util.go
Normal file
@ -0,0 +1,38 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
httpCtx "forge.cadoles.com/wpetit/clearcase/internal/http/context"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/url"
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
var (
|
||||
WithPath = url.WithPath
|
||||
WithoutValues = url.WithoutValues
|
||||
WithValuesReset = url.WithValuesReset
|
||||
WithValues = url.WithValues
|
||||
)
|
||||
|
||||
func BaseURL(ctx context.Context, funcs ...url.MutationFunc) templ.SafeURL {
|
||||
baseURL := httpCtx.BaseURL(ctx)
|
||||
mutated := url.Mutate(baseURL, funcs...)
|
||||
return templ.SafeURL(mutated.String())
|
||||
}
|
||||
|
||||
func CurrentURL(ctx context.Context, funcs ...url.MutationFunc) templ.SafeURL {
|
||||
currentURL := clone(httpCtx.CurrentURL(ctx))
|
||||
mutated := url.Mutate(currentURL, funcs...)
|
||||
return templ.SafeURL(mutated.String())
|
||||
}
|
||||
|
||||
func MatchPath(ctx context.Context, path string) bool {
|
||||
currentURL := httpCtx.CurrentURL(ctx)
|
||||
return currentURL.Path == path
|
||||
}
|
||||
|
||||
func clone[T any](v *T) *T {
|
||||
copy := *v
|
||||
return ©
|
||||
}
|
29
internal/http/handler/webui/common/error.go
Normal file
29
internal/http/handler/webui/common/error.go
Normal file
@ -0,0 +1,29 @@
|
||||
package common
|
||||
|
||||
type Error struct {
|
||||
err string
|
||||
userMessage string
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// StatusCode implements HTTPError.
|
||||
func (e *Error) StatusCode() int {
|
||||
return e.statusCode
|
||||
}
|
||||
|
||||
// Error implements UserFacingError.
|
||||
func (e *Error) Error() string {
|
||||
return e.err
|
||||
}
|
||||
|
||||
// UserMessage implements UserFacingError.
|
||||
func (e *Error) UserMessage() string {
|
||||
return e.userMessage
|
||||
}
|
||||
|
||||
func NewError(err string, userMessage string, statusCode int) *Error {
|
||||
return &Error{err, userMessage, statusCode}
|
||||
}
|
||||
|
||||
var _ UserFacingError = &Error{}
|
||||
var _ HTTPError = &Error{}
|
48
internal/http/handler/webui/common/error_page.go
Normal file
48
internal/http/handler/webui/common/error_page.go
Normal file
@ -0,0 +1,48 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||
"github.com/a-h/templ"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type HTTPError interface {
|
||||
error
|
||||
StatusCode() int
|
||||
}
|
||||
|
||||
type UserFacingError interface {
|
||||
error
|
||||
UserMessage() string
|
||||
}
|
||||
|
||||
func HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
vmodel := component.ErrorPageVModel{}
|
||||
|
||||
statusCode := http.StatusInternalServerError
|
||||
|
||||
var httpErr HTTPError
|
||||
if errors.As(err, &httpErr) {
|
||||
statusCode = httpErr.StatusCode()
|
||||
}
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
var userFacingErr UserFacingError
|
||||
if errors.As(err, &userFacingErr) {
|
||||
vmodel.Message = userFacingErr.UserMessage()
|
||||
} else {
|
||||
vmodel.Message = http.StatusText(statusCode)
|
||||
}
|
||||
|
||||
if httpErr == nil && userFacingErr == nil {
|
||||
slog.ErrorContext(r.Context(), "unexpected error", slog.Any("error", errors.WithStack(err)))
|
||||
}
|
||||
|
||||
errorPage := component.ErrorPage(vmodel)
|
||||
|
||||
templ.Handler(errorPage).ServeHTTP(w, r)
|
||||
}
|
36
internal/http/handler/webui/common/handler.go
Normal file
36
internal/http/handler/webui/common/handler.go
Normal file
@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
var assetsFS embed.FS
|
||||
|
||||
type Handler struct {
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
handler := &Handler{
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
assets, err := fs.Sub(assetsFS, "assets")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
handler.mux.Handle("GET /", http.FileServerFS(assets))
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
var _ http.Handler = &Handler{}
|
36
internal/http/handler/webui/common/util.go
Normal file
36
internal/http/handler/webui/common/util.go
Normal file
@ -0,0 +1,36 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type viewModelFillerFunc[T any] func(ctx context.Context, vmodel *T, r *http.Request) error
|
||||
|
||||
func FillViewModel[T any](ctx context.Context, vmodel *T, r *http.Request, funcs ...viewModelFillerFunc[T]) error {
|
||||
for _, fn := range funcs {
|
||||
if err := fn(ctx, vmodel, r); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DetectMimeType(input io.Reader) (mimeType string, recycled io.Reader, err error) {
|
||||
header := bytes.NewBuffer(nil)
|
||||
|
||||
mtype, err := mimetype.DetectReader(io.TeeReader(input, header))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
recycled = io.MultiReader(header, input)
|
||||
|
||||
return mtype.String(), recycled, err
|
||||
}
|
41
internal/http/handler/webui/handler.go
Normal file
41
internal/http/handler/webui/handler.go
Normal file
@ -0,0 +1,41 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func NewHandler(funcs ...OptionFunc) *Handler {
|
||||
opts := NewOptions(funcs...)
|
||||
|
||||
h := &Handler{
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
for mountpoint, handler := range opts.Mounts {
|
||||
h.mount(mountpoint, handler)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *Handler) mount(prefix string, handler http.Handler) {
|
||||
trimmed := strings.TrimSuffix(prefix, "/")
|
||||
|
||||
if len(trimmed) > 0 {
|
||||
h.mux.Handle(prefix, http.StripPrefix(trimmed, handler))
|
||||
} else {
|
||||
h.mux.Handle(prefix, handler)
|
||||
}
|
||||
}
|
||||
|
||||
var _ http.Handler = &Handler{}
|
@ -0,0 +1,8 @@
|
||||
package component
|
||||
|
||||
import common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||
|
||||
templ IssuePage(title string) {
|
||||
@common.Page(common.WithTitle(title)) {
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.819
|
||||
package component
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
|
||||
|
||||
func IssuePage(title string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = common.Page(common.WithTitle(title)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
26
internal/http/handler/webui/issue/handler.go
Normal file
26
internal/http/handler/webui/issue/handler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package issue
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
h := &Handler{
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
h.mux.HandleFunc("GET /", h.getIssuePage)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
var _ http.Handler = &Handler{}
|
7
internal/http/handler/webui/issue/issue_page.go
Normal file
7
internal/http/handler/webui/issue/issue_page.go
Normal file
@ -0,0 +1,7 @@
|
||||
package issue
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (h *Handler) getIssuePage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
38
internal/http/handler/webui/options.go
Normal file
38
internal/http/handler/webui/options.go
Normal file
@ -0,0 +1,38 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
BaseURL string
|
||||
SessionStore sessions.Store
|
||||
Mounts map[string]http.Handler
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
BaseURL: "",
|
||||
Mounts: make(map[string]http.Handler),
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithBaseURL(baseURL string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.BaseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
func WithMount(mountpoint string, handler http.Handler) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Mounts[mountpoint] = handler
|
||||
}
|
||||
}
|
41
internal/http/options.go
Normal file
41
internal/http/options.go
Normal file
@ -0,0 +1,41 @@
|
||||
package http
|
||||
|
||||
import "github.com/gorilla/sessions"
|
||||
|
||||
type Options struct {
|
||||
Address string
|
||||
BaseURL string
|
||||
SessionStore sessions.Store
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
Address: ":3000",
|
||||
BaseURL: "",
|
||||
SessionStore: sessions.NewCookieStore(),
|
||||
}
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithBaseURL(baseURL string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.BaseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
func WithAddress(addr string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Address = addr
|
||||
}
|
||||
}
|
||||
|
||||
func WithSessionStore(store sessions.Store) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.SessionStore = store
|
||||
}
|
||||
}
|
63
internal/http/server.go
Normal file
63
internal/http/server.go
Normal file
@ -0,0 +1,63 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
sloghttp "github.com/samber/slog-http"
|
||||
|
||||
httpCtx "forge.cadoles.com/wpetit/clearcase/internal/http/context"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
handler http.Handler
|
||||
opts *Options
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
handler := sloghttp.Recovery(s.handler)
|
||||
handler = sloghttp.New(slog.Default())(handler)
|
||||
|
||||
handler = func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
ctx = httpCtx.SetBaseURL(ctx, s.opts.BaseURL)
|
||||
ctx = httpCtx.SetCurrentURL(ctx, r.URL)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}(handler)
|
||||
|
||||
server := http.Server{
|
||||
Addr: s.opts.Address,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if err := server.Close(); err != nil {
|
||||
slog.ErrorContext(ctx, "could not close server", slog.Any("error", errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewServer(handler http.Handler, funcs ...OptionFunc) *Server {
|
||||
opts := NewOptions(funcs...)
|
||||
return &Server{
|
||||
handler: handler,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
110
internal/http/url/mutation.go
Normal file
110
internal/http/url/mutation.go
Normal file
@ -0,0 +1,110 @@
|
||||
package url
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type URL = url.URL
|
||||
|
||||
var Parse = url.Parse
|
||||
|
||||
func Mutate(u *url.URL, funcs ...MutationFunc) *url.URL {
|
||||
cloned := clone(u)
|
||||
|
||||
for _, fn := range funcs {
|
||||
fn(cloned)
|
||||
}
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
type MutationFunc func(u *url.URL)
|
||||
|
||||
func keyValuesToValues(kv []string) url.Values {
|
||||
if len(kv)%2 != 0 {
|
||||
panic(errors.New("expected pair number of key/values"))
|
||||
}
|
||||
|
||||
values := make(url.Values)
|
||||
|
||||
var key string
|
||||
for idx := range kv {
|
||||
if idx%2 == 0 {
|
||||
key = kv[idx]
|
||||
continue
|
||||
}
|
||||
|
||||
values.Add(key, kv[idx])
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func WithValues(kv ...string) MutationFunc {
|
||||
values := keyValuesToValues(kv)
|
||||
|
||||
return func(u *url.URL) {
|
||||
query := u.Query()
|
||||
|
||||
for k, vv := range values {
|
||||
for _, v := range vv {
|
||||
query.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
u.RawQuery = query.Encode()
|
||||
}
|
||||
}
|
||||
|
||||
func WithValuesReset() MutationFunc {
|
||||
return func(u *url.URL) {
|
||||
u.RawQuery = ""
|
||||
}
|
||||
}
|
||||
|
||||
func WithoutValues(kv ...string) MutationFunc {
|
||||
toDelete := keyValuesToValues(kv)
|
||||
|
||||
return func(u *url.URL) {
|
||||
query := u.Query()
|
||||
|
||||
for keyToDelete, valuesToDelete := range toDelete {
|
||||
values, keyExists := query[keyToDelete]
|
||||
if !keyExists {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, d := range valuesToDelete {
|
||||
if d == "*" {
|
||||
query.Del(keyToDelete)
|
||||
break
|
||||
}
|
||||
|
||||
query[keyToDelete] = slices.DeleteFunc(values, func(value string) bool {
|
||||
return value == d
|
||||
})
|
||||
}
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
}
|
||||
}
|
||||
|
||||
func WithPath(paths ...string) MutationFunc {
|
||||
return func(u *url.URL) {
|
||||
u.Path = filepath.Join(paths...)
|
||||
}
|
||||
}
|
||||
|
||||
func applyURLMutations(u *url.URL, funcs []MutationFunc) {
|
||||
for _, fn := range funcs {
|
||||
fn(u)
|
||||
}
|
||||
}
|
||||
|
||||
func clone[T any](v *T) *T {
|
||||
copy := *v
|
||||
return ©
|
||||
}
|
138
internal/setup/auth_handler.go
Normal file
138
internal/setup/auth_handler.go
Normal file
@ -0,0 +1,138 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/config"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/auth"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/markbates/goth/providers/github"
|
||||
"github.com/markbates/goth/providers/google"
|
||||
"github.com/markbates/goth/providers/openidConnect"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NewAuthHandlerFromConfig(ctx context.Context, conf *config.Config) (*auth.Handler, error) {
|
||||
// Configure sessions store
|
||||
|
||||
keyPairs := make([][]byte, 0)
|
||||
if len(conf.HTTP.Session.Keys) == 0 {
|
||||
key, err := getRandomBytes(32)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not generate cookie signing key")
|
||||
}
|
||||
|
||||
keyPairs = append(keyPairs, key)
|
||||
} else {
|
||||
for _, k := range conf.HTTP.Session.Keys {
|
||||
keyPairs = append(keyPairs, []byte(k))
|
||||
}
|
||||
}
|
||||
|
||||
sessionStore := sessions.NewCookieStore(keyPairs...)
|
||||
|
||||
sessionStore.MaxAge(int(conf.HTTP.Session.Cookie.MaxAge))
|
||||
sessionStore.Options.Path = conf.HTTP.Session.Cookie.Path
|
||||
sessionStore.Options.HttpOnly = conf.HTTP.Session.Cookie.HTTPOnly
|
||||
sessionStore.Options.Secure = conf.HTTP.Session.Cookie.Secure
|
||||
|
||||
// Configure providers
|
||||
|
||||
gothProviders := make([]goth.Provider, 0)
|
||||
providers := make([]auth.Provider, 0)
|
||||
|
||||
if conf.Auth.Providers.Google.Key != "" && conf.Auth.Providers.Google.Secret != "" {
|
||||
googleProvider := google.New(
|
||||
conf.Auth.Providers.Google.Key,
|
||||
conf.Auth.Providers.Google.Secret,
|
||||
fmt.Sprintf("%s/auth/providers/google/callback", conf.HTTP.BaseURL),
|
||||
conf.Auth.Providers.Google.Scopes...,
|
||||
)
|
||||
|
||||
gothProviders = append(gothProviders, googleProvider)
|
||||
|
||||
providers = append(providers, auth.Provider{
|
||||
ID: googleProvider.Name(),
|
||||
Label: "Google",
|
||||
Icon: "fa-google",
|
||||
})
|
||||
}
|
||||
|
||||
if conf.Auth.Providers.Github.Key != "" && conf.Auth.Providers.Github.Secret != "" {
|
||||
githubProvider := github.New(
|
||||
conf.Auth.Providers.Github.Key,
|
||||
conf.Auth.Providers.Github.Secret,
|
||||
fmt.Sprintf("%s/auth/providers/github/callback", conf.HTTP.BaseURL),
|
||||
conf.Auth.Providers.Github.Scopes...,
|
||||
)
|
||||
|
||||
gothProviders = append(gothProviders, githubProvider)
|
||||
|
||||
providers = append(providers, auth.Provider{
|
||||
ID: githubProvider.Name(),
|
||||
Label: "Github",
|
||||
Icon: "fa-github",
|
||||
})
|
||||
}
|
||||
|
||||
if conf.Auth.Providers.OIDC.Key != "" && conf.Auth.Providers.OIDC.Secret != "" {
|
||||
oidcProvider, err := openidConnect.New(
|
||||
conf.Auth.Providers.OIDC.Key,
|
||||
conf.Auth.Providers.OIDC.Secret,
|
||||
fmt.Sprintf("%s/auth/providers/openid-connect/callback", conf.HTTP.BaseURL),
|
||||
conf.Auth.Providers.OIDC.DiscoveryURL,
|
||||
conf.Auth.Providers.OIDC.Scopes...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not configure oidc provider")
|
||||
}
|
||||
|
||||
gothProviders = append(gothProviders, oidcProvider)
|
||||
|
||||
providers = append(providers, auth.Provider{
|
||||
ID: oidcProvider.Name(),
|
||||
Label: conf.Auth.Providers.OIDC.Label,
|
||||
Icon: conf.Auth.Providers.OIDC.Icon,
|
||||
})
|
||||
}
|
||||
|
||||
goth.UseProviders(gothProviders...)
|
||||
gothic.Store = sessionStore
|
||||
|
||||
opts := []auth.OptionFunc{
|
||||
auth.WithProviders(providers...),
|
||||
}
|
||||
|
||||
if conf.Auth.DefaultAdmin.Email != "" && conf.Auth.DefaultAdmin.Provider != "" {
|
||||
opts = append(opts, auth.WithDefaultAdmin(auth.DefaultAdmin{
|
||||
Provider: conf.Auth.DefaultAdmin.Provider,
|
||||
Email: conf.Auth.DefaultAdmin.Email,
|
||||
}))
|
||||
}
|
||||
|
||||
auth := auth.NewHandler(
|
||||
sessionStore,
|
||||
opts...,
|
||||
)
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func getRandomBytes(n int) ([]byte, error) {
|
||||
data := make([]byte, n)
|
||||
|
||||
read, err := rand.Read(data)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if read != n {
|
||||
return nil, errors.Errorf("could not read %d bytes", n)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
27
internal/setup/http_server.go
Normal file
27
internal/setup/http_server.go
Normal file
@ -0,0 +1,27 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/config"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NewHTTPServerFromConfig(ctx context.Context, conf *config.Config) (*http.Server, error) {
|
||||
// Configure Web UI handler
|
||||
webui, err := NewWebUIHandlerFromConfig(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not configure webui handler from config")
|
||||
}
|
||||
|
||||
// Create HTTP server
|
||||
|
||||
server := http.NewServer(
|
||||
webui,
|
||||
http.WithAddress(conf.HTTP.Address),
|
||||
http.WithBaseURL(conf.HTTP.BaseURL),
|
||||
)
|
||||
|
||||
return server, nil
|
||||
}
|
12
internal/setup/issue_handler.go
Normal file
12
internal/setup/issue_handler.go
Normal file
@ -0,0 +1,12 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/config"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/issue"
|
||||
)
|
||||
|
||||
func NewIssueHandlerFromConfig(ctx context.Context, conf *config.Config) (*issue.Handler, error) {
|
||||
return issue.NewHandler(), nil
|
||||
}
|
44
internal/setup/registry.go
Normal file
44
internal/setup/registry.go
Normal file
@ -0,0 +1,44 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrNotRegistered = errors.New("not registered")
|
||||
|
||||
type Factory[T any] func(u *url.URL) (T, error)
|
||||
|
||||
type Registry[T any] struct {
|
||||
mappings map[string]Factory[T]
|
||||
}
|
||||
|
||||
func (r *Registry[T]) Register(scheme string, factory Factory[T]) {
|
||||
r.mappings[scheme] = factory
|
||||
}
|
||||
|
||||
func (r *Registry[T]) From(rawURL string) (T, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
factory, exists := r.mappings[u.Scheme]
|
||||
if !exists {
|
||||
return *new(T), errors.Wrapf(ErrNotRegistered, "scheme '%s' not found", u.Scheme)
|
||||
}
|
||||
|
||||
value, err := factory(u)
|
||||
if err != nil {
|
||||
return *new(T), errors.WithStack(err)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func NewRegistry[T any]() *Registry[T] {
|
||||
return &Registry[T]{
|
||||
mappings: make(map[string]Factory[T]),
|
||||
}
|
||||
}
|
48
internal/setup/webui_handler.go
Normal file
48
internal/setup/webui_handler.go
Normal file
@ -0,0 +1,48 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/config"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NewWebUIHandlerFromConfig(ctx context.Context, conf *config.Config) (*webui.Handler, error) {
|
||||
opts := make([]webui.OptionFunc, 0)
|
||||
|
||||
// Configure auth handler
|
||||
authHandler, err := NewAuthHandlerFromConfig(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not configure auth handler from config")
|
||||
}
|
||||
|
||||
authMiddleware := authHandler.Middleware()
|
||||
|
||||
opts = append(opts, webui.WithMount("/auth/", authHandler))
|
||||
|
||||
// Configure issue handler
|
||||
issueHandler, err := NewIssueHandlerFromConfig(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not configure explorer handler from config")
|
||||
}
|
||||
|
||||
opts = append(opts, webui.WithMount("/", authMiddleware(issueHandler)))
|
||||
|
||||
// Configure common handler
|
||||
commonHandler, err := NewCommonHandlerFromConfig(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not configure common handler from config")
|
||||
}
|
||||
|
||||
opts = append(opts, webui.WithMount("/assets/", commonHandler))
|
||||
|
||||
handler := webui.NewHandler(opts...)
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
func NewCommonHandlerFromConfig(ctx context.Context, conf *config.Config) (*common.Handler, error) {
|
||||
return common.NewHandler(), nil
|
||||
}
|
40
misc/docker/Dockerfile
Normal file
40
misc/docker/Dockerfile
Normal file
@ -0,0 +1,40 @@
|
||||
FROM golang:1.23 AS build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y make git
|
||||
|
||||
RUN --mount=type=cache,target=/go/pkg/mod/ \
|
||||
--mount=type=bind,source=go.sum,target=go.sum \
|
||||
--mount=type=bind,source=go.mod,target=go.mod \
|
||||
go mod download -x
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY . /src
|
||||
|
||||
ARG GOCACHE=/root/.cache/go-build
|
||||
|
||||
RUN --mount=type=cache,target=/go/pkg/mod/ \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=cache,target=./tools \
|
||||
make build
|
||||
|
||||
FROM alpine:3.21 AS runtime
|
||||
|
||||
RUN apk add \
|
||||
ca-certificates \
|
||||
openssl \
|
||||
openjdk21-jdk \
|
||||
msttcorefonts-installer \
|
||||
ttf-dejavu \
|
||||
fontconfig \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-data-eng \
|
||||
tesseract-ocr-data-fra \
|
||||
&& update-ms-fonts \
|
||||
&& fc-cache -f -v \
|
||||
&& update-ca-certificates
|
||||
|
||||
COPY --from=build /src/bin/clearcase /usr/local/bin/clearcase
|
||||
|
||||
CMD ["/usr/local/bin/clearcase"]
|
8
misc/docker/docker.mk
Normal file
8
misc/docker/docker.mk
Normal file
@ -0,0 +1,8 @@
|
||||
DOCKER_STANDALONE_IMAGE_NAME := clearcase-standalone
|
||||
DOCKER_STANDALONE_IMAGE_TAG := latest
|
||||
|
||||
docker-image:
|
||||
docker build -f misc/docker/Dockerfile -t $(DOCKER_STANDALONE_IMAGE_NAME):$(DOCKER_STANDALONE_IMAGE_TAG) .
|
||||
|
||||
docker-run:
|
||||
docker run -it --rm -p 3000:3000 --name rkvst --tmpfs /tmp --env-file .env $(DOCKER_STANDALONE_IMAGE_NAME):$(DOCKER_STANDALONE_IMAGE_TAG)
|
Loading…
x
Reference in New Issue
Block a user