Compare commits
2 Commits
1d9af44d0a
...
1e09acdc57
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e09acdc57 | |||
| 03095448d6 |
@@ -5,7 +5,7 @@ tmp_dir = "tmp"
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ./cmd/reshoot"
|
||||
cmd = "go build -o ./tmp/main ./cmd/projectreshoot"
|
||||
delay = 1000
|
||||
exclude_dir = []
|
||||
exclude_file = []
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,5 +5,5 @@ server.log
|
||||
bin/
|
||||
tmp/
|
||||
static/css/output.css
|
||||
view/**/*_templ.go
|
||||
view/**/*_templ.txt
|
||||
internal/view/**/*_templ.go
|
||||
internal/view/**/*_templ.txt
|
||||
|
||||
20
Makefile
20
Makefile
@@ -8,26 +8,18 @@ build:
|
||||
tailwindcss -i ./pkg/embedfs/files/css/input.css -o ./pkg/embedfs/files/css/output.css && \
|
||||
go mod tidy && \
|
||||
templ generate && \
|
||||
go generate ./cmd/projectreshoot && \
|
||||
go build -ldflags="-w -s" -o ./bin/${BINARY_NAME}${SUFFIX} ./cmd/projectreshoot
|
||||
go generate ./cmd/${BINARY_NAME} && \
|
||||
go build -ldflags="-w -s" -o ./bin/${BINARY_NAME}${SUFFIX} ./cmd/${BINARY_NAME}
|
||||
|
||||
run:
|
||||
make build
|
||||
./bin/${BINARY_NAME}${SUFFIX}
|
||||
|
||||
dev:
|
||||
templ generate --watch &\
|
||||
air &\
|
||||
tailwindcss -i ./pkg/embedfs/files/css/input.css -o ./pkg/embedfs/files/css/output.css --watch
|
||||
|
||||
tester:
|
||||
go mod tidy && \
|
||||
go run . --port 3232 --tester --loglevel trace
|
||||
|
||||
test:
|
||||
go mod tidy && \
|
||||
templ generate && \
|
||||
go generate ./cmd/projectreshoot && \
|
||||
go test ./cmd/projectreshoot
|
||||
go test ./pkg/db
|
||||
go test ./internal/middleware
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
|
||||
@@ -1,37 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/db"
|
||||
"projectreshoot/pkg/tests"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func setupDBConn(
|
||||
args map[string]string,
|
||||
logger *zerolog.Logger,
|
||||
config *config.Config,
|
||||
) (*db.SafeConn, error) {
|
||||
if args["test"] == "true" {
|
||||
logger.Debug().Msg("Server in test mode, using test database")
|
||||
ver, err := strconv.ParseInt(config.DBName, 10, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "strconv.ParseInt")
|
||||
}
|
||||
wconn, rconn, err := tests.SetupTestDB(ver)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tests.SetupTestDB")
|
||||
}
|
||||
conn := db.MakeSafe(wconn, rconn, logger)
|
||||
return conn, nil
|
||||
} else {
|
||||
conn, err := db.ConnectToDatabase(config.DBName, logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.ConnectToDatabase")
|
||||
}
|
||||
return conn, nil
|
||||
func setupDBConn(dbName string) (*sql.DB, error) {
|
||||
opts := "_journal_mode=WAL&_synchronous=NORMAL&_txlock=IMMEDIATE"
|
||||
file := fmt.Sprintf("file:%s.db?%s", dbName, opts)
|
||||
conn, err := sql.Open("sqlite3", file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sql.Open")
|
||||
}
|
||||
err = checkDBVersion(conn, dbName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "checkDBVersion")
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Check the database version
|
||||
func checkDBVersion(db *sql.DB, dbName string) error {
|
||||
expectVer, err := strconv.Atoi(dbName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "strconv.Atoi")
|
||||
}
|
||||
query := `SELECT version_id FROM goose_db_version WHERE is_applied = 1
|
||||
ORDER BY version_id DESC LIMIT 1`
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "db.Query")
|
||||
}
|
||||
defer rows.Close()
|
||||
if rows.Next() {
|
||||
var version int
|
||||
err = rows.Scan(&version)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "rows.Scan")
|
||||
}
|
||||
if version != expectVer {
|
||||
return errors.New("Version mismatch")
|
||||
}
|
||||
} else {
|
||||
return errors.New("No version found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_main(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
t.Cleanup(cancel)
|
||||
args := map[string]string{"test": "true"}
|
||||
var stdout bytes.Buffer
|
||||
os.Setenv("SECRET_KEY", ".")
|
||||
os.Setenv("TMDB_API_TOKEN", ".")
|
||||
os.Setenv("HOST", "127.0.0.1")
|
||||
os.Setenv("PORT", "3232")
|
||||
runSrvErr := make(chan error)
|
||||
go func() {
|
||||
if err := run(ctx, &stdout, args); err != nil {
|
||||
runSrvErr <- err
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := waitForReady(ctx, 10*time.Second, "http://127.0.0.1:3232/healthz")
|
||||
if err != nil {
|
||||
runSrvErr <- err
|
||||
return
|
||||
}
|
||||
runSrvErr <- nil
|
||||
}()
|
||||
select {
|
||||
case err := <-runSrvErr:
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting test server: %s", err)
|
||||
return
|
||||
}
|
||||
t.Log("Test server started")
|
||||
}
|
||||
|
||||
t.Run("SIGUSR1 puts database into global lock", func(t *testing.T) {
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
expected := "Global database lock acquired"
|
||||
for {
|
||||
if strings.Contains(stdout.String(), expected) {
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
proc, err := os.FindProcess(os.Getpid())
|
||||
require.NoError(t, err)
|
||||
proc.Signal(syscall.SIGUSR1)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
t.Log("found")
|
||||
case <-time.After(250 * time.Millisecond):
|
||||
t.Errorf("Not found")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SIGUSR2 releases database global lock", func(t *testing.T) {
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
expected := "Global database lock released"
|
||||
for {
|
||||
if strings.Contains(stdout.String(), expected) {
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
proc, err := os.FindProcess(os.Getpid())
|
||||
require.NoError(t, err)
|
||||
proc.Signal(syscall.SIGUSR2)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
t.Log("found")
|
||||
case <-time.After(250 * time.Millisecond):
|
||||
t.Errorf("Not found")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func waitForReady(
|
||||
ctx context.Context,
|
||||
timeout time.Duration,
|
||||
endpoint string,
|
||||
) error {
|
||||
client := http.Client{}
|
||||
startTime := time.Now()
|
||||
for {
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodGet,
|
||||
endpoint,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Error making request: %s\n", err.Error())
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
fmt.Println("Endpoint is ready!")
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
if time.Since(startTime) >= timeout {
|
||||
return fmt.Errorf("timeout reached while waiting for endpoint")
|
||||
}
|
||||
// wait a little while between checks
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,12 @@ import (
|
||||
"projectreshoot/internal/httpserver"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/embedfs"
|
||||
"projectreshoot/pkg/logging"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -38,7 +40,7 @@ func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
||||
// Setup the logfile
|
||||
var logfile *os.File = nil
|
||||
if config.LogOutput == "both" || config.LogOutput == "file" {
|
||||
logfile, err = logging.GetLogFile(config.LogDir)
|
||||
logfile, err = hlog.NewLogFile(config.LogDir)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "logging.GetLogFile")
|
||||
}
|
||||
@@ -52,7 +54,7 @@ func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
||||
}
|
||||
|
||||
// Setup the logger
|
||||
logger, err := logging.GetLogger(
|
||||
logger, err := hlog.NewLogger(
|
||||
config.LogLevel,
|
||||
consoleWriter,
|
||||
logfile,
|
||||
@@ -65,7 +67,7 @@ func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
||||
// Setup the database connection
|
||||
logger.Debug().Msg("Config loaded and logger started")
|
||||
logger.Debug().Msg("Connecting to database")
|
||||
conn, err := setupDBConn(args, logger, config)
|
||||
conn, err := setupDBConn(config.DBName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "setupDBConn")
|
||||
}
|
||||
@@ -78,18 +80,22 @@ func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
||||
return errors.Wrap(err, "getStaticFiles")
|
||||
}
|
||||
|
||||
logger.Debug().Msg("Setting up HTTP server")
|
||||
httpServer := httpserver.NewServer(config, logger, conn, &staticFS, &maint)
|
||||
// Setup TokenGenerator
|
||||
logger.Debug().Msg("Creating TokenGenerator")
|
||||
tokenGen, err := jwt.CreateGenerator(
|
||||
config.AccessTokenExpiry,
|
||||
config.RefreshTokenExpiry,
|
||||
config.TokenFreshTime,
|
||||
config.TrustedHost,
|
||||
config.SecretKey,
|
||||
conn,
|
||||
)
|
||||
|
||||
// Runs function for testing in dev if --tester flag true
|
||||
if args["tester"] == "true" {
|
||||
logger.Debug().Msg("Running tester function")
|
||||
test(config, logger, conn, httpServer)
|
||||
return nil
|
||||
}
|
||||
logger.Debug().Msg("Setting up HTTP server")
|
||||
httpServer := httpserver.NewServer(config, logger, conn, tokenGen, &staticFS, &maint)
|
||||
|
||||
// Setups a channel to listen for os.Signal
|
||||
handleMaintSignals(conn, config, httpServer, logger)
|
||||
handleMaintSignals(httpServer, logger)
|
||||
|
||||
// Runs the http server
|
||||
logger.Debug().Msg("Starting up the HTTP server")
|
||||
@@ -102,9 +108,7 @@ func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
||||
|
||||
// Handles graceful shutdown
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
<-ctx.Done()
|
||||
shutdownCtx := context.Background()
|
||||
shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second)
|
||||
@@ -112,7 +116,7 @@ func run(ctx context.Context, w io.Writer, args map[string]string) error {
|
||||
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Error().Err(err).Msg("Error shutting down server")
|
||||
}
|
||||
}()
|
||||
})
|
||||
wg.Wait()
|
||||
logger.Info().Msg("Shutting down")
|
||||
return nil
|
||||
|
||||
@@ -4,21 +4,16 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/db"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
)
|
||||
|
||||
// Handle SIGUSR1 and SIGUSR2 syscalls to toggle maintenance mode
|
||||
func handleMaintSignals(
|
||||
conn *db.SafeConn,
|
||||
config *config.Config,
|
||||
srv *http.Server,
|
||||
logger *zerolog.Logger,
|
||||
logger *hlog.Logger,
|
||||
) {
|
||||
logger.Debug().Msg("Starting signal listener")
|
||||
ch := make(chan os.Signal, 1)
|
||||
@@ -33,14 +28,10 @@ func handleMaintSignals(
|
||||
if atomic.LoadUint32(&maint) != 1 {
|
||||
atomic.StoreUint32(&maint, 1)
|
||||
logger.Info().Msg("Signal received: Starting maintenance")
|
||||
logger.Info().Msg("Attempting to acquire database lock")
|
||||
conn.Pause(config.DBLockTimeout * time.Second)
|
||||
}
|
||||
case syscall.SIGUSR2:
|
||||
if atomic.LoadUint32(&maint) != 0 {
|
||||
logger.Info().Msg("Signal received: Maintenance over")
|
||||
logger.Info().Msg("Releasing database lock")
|
||||
conn.Resume()
|
||||
atomic.StoreUint32(&maint, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/db"
|
||||
"projectreshoot/pkg/tmdb"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// This function will only be called if the --test commandline flag is set.
|
||||
// After the function finishes the application will close.
|
||||
// Running command `make tester` will run the test using port 3232 to avoid
|
||||
// conflicts on the default 3333. Useful for testing things out during dev.
|
||||
// If you add code here, remember to run:
|
||||
// `git update-index --assume-unchanged tester.go` to avoid tracking changes
|
||||
func test(
|
||||
config *config.Config,
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
srv *http.Server,
|
||||
) {
|
||||
query := "a few good men"
|
||||
search, err := tmdb.SearchMovies(config.TMDBToken, query, false, 1)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("error occured")
|
||||
return
|
||||
}
|
||||
logger.Info().Interface("results", search).Msg("search results received")
|
||||
}
|
||||
24
go.mod
24
go.mod
@@ -1,37 +1,37 @@
|
||||
module projectreshoot
|
||||
|
||||
go 1.24.0
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.833
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
git.haelnorr.com/h/golib/hlog v0.9.0
|
||||
git.haelnorr.com/h/golib/jwt v0.9.0
|
||||
git.haelnorr.com/h/golib/tmdb v0.8.0
|
||||
github.com/a-h/templ v0.3.977
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pressly/goose/v3 v3.24.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
modernc.org/sqlite v1.35.0
|
||||
)
|
||||
|
||||
replace git.haelnorr.com/h/golib/jwt v0.9.0 => /home/haelnorr/projects/golib/jwt
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
modernc.org/libc v1.61.13 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.8.2 // indirect
|
||||
|
||||
44
go.sum
44
go.sum
@@ -1,5 +1,11 @@
|
||||
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
|
||||
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
|
||||
git.haelnorr.com/h/golib/hlog v0.9.0 h1:ib8n2MdmiRK2TF067p220kXmhDe9aAnlcsgpuv+QpvE=
|
||||
git.haelnorr.com/h/golib/hlog v0.9.0/go.mod h1:oOlzb8UVHUYP1k7dN5PSJXVskAB2z8EYgRN85jAi0Zk=
|
||||
git.haelnorr.com/h/golib/tmdb v0.8.0 h1:OQ6M2TB8FHm8fJD7/ebfWm63Duzfp0kmFX9genEig34=
|
||||
git.haelnorr.com/h/golib/tmdb v0.8.0/go.mod h1:mGKYa3o3z0IsQ5EO3MPmnL2Bwl2sSMsUHXVgaIGR7Z0=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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=
|
||||
@@ -16,11 +22,6 @@ 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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
@@ -42,33 +43,30 @@ github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4
|
||||
github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
|
||||
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,10 +11,10 @@ import (
|
||||
"projectreshoot/internal/view/page"
|
||||
"projectreshoot/pkg/contexts"
|
||||
"projectreshoot/pkg/cookies"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Renders the account page on the 'General' subpage
|
||||
@@ -44,8 +45,8 @@ func AccountSubpage() http.Handler {
|
||||
|
||||
// Handles a request to change the users username
|
||||
func ChangeUsername(
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
logger *hlog.Logger,
|
||||
conn *sql.DB,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -53,7 +54,7 @@ func ChangeUsername(
|
||||
defer cancel()
|
||||
|
||||
// Start the transaction
|
||||
tx, err := conn.Begin(ctx)
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Error updating username")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
@@ -61,7 +62,7 @@ func ChangeUsername(
|
||||
}
|
||||
r.ParseForm()
|
||||
newUsername := r.FormValue("username")
|
||||
unique, err := models.CheckUsernameUnique(ctx, tx, newUsername)
|
||||
unique, err := models.CheckUsernameUnique(tx, newUsername)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
logger.Error().Err(err).Msg("Error updating username")
|
||||
@@ -75,7 +76,7 @@ func ChangeUsername(
|
||||
return
|
||||
}
|
||||
user := contexts.GetUser(r.Context())
|
||||
err = user.ChangeUsername(ctx, tx, newUsername)
|
||||
err = user.ChangeUsername(tx, newUsername)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
logger.Error().Err(err).Msg("Error updating username")
|
||||
@@ -90,8 +91,8 @@ func ChangeUsername(
|
||||
|
||||
// Handles a request to change the users bio
|
||||
func ChangeBio(
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
logger *hlog.Logger,
|
||||
conn *sql.DB,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -99,7 +100,7 @@ func ChangeBio(
|
||||
defer cancel()
|
||||
|
||||
// Start the transaction
|
||||
tx, err := conn.Begin(ctx)
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Error updating bio")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
@@ -115,7 +116,7 @@ func ChangeBio(
|
||||
return
|
||||
}
|
||||
user := contexts.GetUser(r.Context())
|
||||
err = user.ChangeBio(ctx, tx, newBio)
|
||||
err = user.ChangeBio(tx, newBio)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
logger.Error().Err(err).Msg("Error updating bio")
|
||||
@@ -144,8 +145,8 @@ func validateChangePassword(
|
||||
|
||||
// Handles a request to change the users password
|
||||
func ChangePassword(
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
logger *hlog.Logger,
|
||||
conn *sql.DB,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -153,7 +154,7 @@ func ChangePassword(
|
||||
defer cancel()
|
||||
|
||||
// Start the transaction
|
||||
tx, err := conn.Begin(ctx)
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Error updating password")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
@@ -166,7 +167,7 @@ func ChangePassword(
|
||||
return
|
||||
}
|
||||
user := contexts.GetUser(r.Context())
|
||||
err = user.SetPassword(ctx, tx, newPass)
|
||||
err = user.SetPassword(tx, newPass)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
logger.Error().Err(err).Msg("Error updating password")
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,27 +11,26 @@ import (
|
||||
"projectreshoot/internal/view/page"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/cookies"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Validates the username matches a user in the database and the password
|
||||
// is correct. Returns the corresponding user
|
||||
func validateLogin(
|
||||
ctx context.Context,
|
||||
tx db.SafeTX,
|
||||
tx *sql.Tx,
|
||||
r *http.Request,
|
||||
) (*models.User, error) {
|
||||
formUsername := r.FormValue("username")
|
||||
formPassword := r.FormValue("password")
|
||||
user, err := models.GetUserFromUsername(ctx, tx, formUsername)
|
||||
user, err := models.GetUserFromUsername(tx, formUsername)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.GetUserFromUsername")
|
||||
}
|
||||
|
||||
err = user.CheckPassword(formPassword)
|
||||
err = user.CheckPassword(tx, formPassword)
|
||||
if err != nil {
|
||||
return nil, errors.New("Username or password incorrect")
|
||||
}
|
||||
@@ -52,8 +52,9 @@ func checkRememberMe(r *http.Request) bool {
|
||||
// template for user feedback
|
||||
func LoginRequest(
|
||||
config *config.Config,
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
logger *hlog.Logger,
|
||||
conn *sql.DB,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -61,14 +62,14 @@ func LoginRequest(
|
||||
defer cancel()
|
||||
|
||||
// Start the transaction
|
||||
tx, err := conn.Begin(ctx)
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Failed to set token cookies")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
r.ParseForm()
|
||||
user, err := validateLogin(ctx, tx, r)
|
||||
user, err := validateLogin(tx, r)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
if err.Error() != "Username or password incorrect" {
|
||||
@@ -81,7 +82,7 @@ func LoginRequest(
|
||||
}
|
||||
|
||||
rememberMe := checkRememberMe(r)
|
||||
err = cookies.SetTokenCookies(w, r, config, user, true, rememberMe)
|
||||
err = cookies.SetTokenCookies(w, r, config, tokenGen, user, true, rememberMe)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -2,26 +2,25 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/cookies"
|
||||
"projectreshoot/pkg/db"
|
||||
"projectreshoot/pkg/jwt"
|
||||
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func revokeAccess(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
tx *sql.Tx,
|
||||
atStr string,
|
||||
) error {
|
||||
aT, err := jwt.ParseAccessToken(config, ctx, tx, atStr)
|
||||
aT, err := tokenGen.ValidateAccess(tx, atStr)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Token is expired") ||
|
||||
strings.Contains(err.Error(), "Token has been revoked") {
|
||||
@@ -29,7 +28,7 @@ func revokeAccess(
|
||||
}
|
||||
return errors.Wrap(err, "jwt.ParseAccessToken")
|
||||
}
|
||||
err = jwt.RevokeToken(ctx, tx, aT)
|
||||
err = aT.Revoke(tx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "jwt.RevokeToken")
|
||||
}
|
||||
@@ -37,12 +36,11 @@ func revokeAccess(
|
||||
}
|
||||
|
||||
func revokeRefresh(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
tx *sql.Tx,
|
||||
rtStr string,
|
||||
) error {
|
||||
rT, err := jwt.ParseRefreshToken(config, ctx, tx, rtStr)
|
||||
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Token is expired") ||
|
||||
strings.Contains(err.Error(), "Token has been revoked") {
|
||||
@@ -50,7 +48,7 @@ func revokeRefresh(
|
||||
}
|
||||
return errors.Wrap(err, "jwt.ParseRefreshToken")
|
||||
}
|
||||
err = jwt.RevokeToken(ctx, tx, rT)
|
||||
err = rT.Revoke(tx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "jwt.RevokeToken")
|
||||
}
|
||||
@@ -59,20 +57,19 @@ func revokeRefresh(
|
||||
|
||||
// Retrieve and revoke the user's tokens
|
||||
func revokeTokens(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
tx *sql.Tx,
|
||||
r *http.Request,
|
||||
) error {
|
||||
// get the tokens from the cookies
|
||||
atStr, rtStr := cookies.GetTokenStrings(r)
|
||||
// revoke the refresh token first as the access token expires quicker
|
||||
// only matters if there is an error revoking the tokens
|
||||
err := revokeRefresh(config, ctx, tx, rtStr)
|
||||
err := revokeRefresh(tokenGen, tx, rtStr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "revokeRefresh")
|
||||
}
|
||||
err = revokeAccess(config, ctx, tx, atStr)
|
||||
err = revokeAccess(tokenGen, tx, atStr)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "revokeAccess")
|
||||
}
|
||||
@@ -81,25 +78,25 @@ func revokeTokens(
|
||||
|
||||
// Handle a logout request
|
||||
func Logout(
|
||||
config *config.Config,
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
conn *sql.DB,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
logger *hlog.Logger,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Start the transaction
|
||||
tx, err := conn.Begin(ctx)
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Error occured on user logout")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
logger.Error().Err(err).Msg("Failed to start database transaction")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = revokeTokens(config, ctx, tx, r)
|
||||
defer tx.Rollback()
|
||||
|
||||
err = revokeTokens(tokenGen, tx, r)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
logger.Error().Err(err).Msg("Error occured on user logout")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -4,14 +4,14 @@ import (
|
||||
"net/http"
|
||||
"projectreshoot/internal/view/page"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/tmdb"
|
||||
"strconv"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/tmdb"
|
||||
)
|
||||
|
||||
func Movie(
|
||||
logger *zerolog.Logger,
|
||||
logger *hlog.Logger,
|
||||
config *config.Config,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"projectreshoot/internal/view/component/search"
|
||||
"projectreshoot/internal/view/page"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/tmdb"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/tmdb"
|
||||
)
|
||||
|
||||
func SearchMovies(
|
||||
logger *zerolog.Logger,
|
||||
logger *hlog.Logger,
|
||||
config *config.Config,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -9,47 +10,45 @@ import (
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/contexts"
|
||||
"projectreshoot/pkg/cookies"
|
||||
"projectreshoot/pkg/db"
|
||||
"projectreshoot/pkg/jwt"
|
||||
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Get the tokens from the request
|
||||
func getTokens(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx db.SafeTX,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
tx *sql.Tx,
|
||||
r *http.Request,
|
||||
) (*jwt.AccessToken, *jwt.RefreshToken, error) {
|
||||
// get the existing tokens from the cookies
|
||||
atStr, rtStr := cookies.GetTokenStrings(r)
|
||||
aT, err := jwt.ParseAccessToken(config, ctx, tx, atStr)
|
||||
aT, err := tokenGen.ValidateAccess(tx, atStr)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "jwt.ParseAccessToken")
|
||||
return nil, nil, errors.Wrap(err, "tokenGen.ValidateAccess")
|
||||
}
|
||||
rT, err := jwt.ParseRefreshToken(config, ctx, tx, rtStr)
|
||||
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "jwt.ParseRefreshToken")
|
||||
return nil, nil, errors.Wrap(err, "tokenGen.ValidateRefresh")
|
||||
}
|
||||
return aT, rT, nil
|
||||
}
|
||||
|
||||
// Revoke the given token pair
|
||||
func revokeTokenPair(
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tx *sql.Tx,
|
||||
aT *jwt.AccessToken,
|
||||
rT *jwt.RefreshToken,
|
||||
) error {
|
||||
err := jwt.RevokeToken(ctx, tx, aT)
|
||||
err := aT.Revoke(tx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "jwt.RevokeToken")
|
||||
return errors.Wrap(err, "aT.Revoke")
|
||||
}
|
||||
err = jwt.RevokeToken(ctx, tx, rT)
|
||||
err = rT.Revoke(tx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "jwt.RevokeToken")
|
||||
return errors.Wrap(err, "rT.Revoke")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -57,12 +56,12 @@ func revokeTokenPair(
|
||||
// Issue new tokens for the user, invalidating the old ones
|
||||
func refreshTokens(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
tx *sql.Tx,
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
) error {
|
||||
aT, rT, err := getTokens(config, ctx, tx, r)
|
||||
aT, rT, err := getTokens(tokenGen, tx, r)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getTokens")
|
||||
}
|
||||
@@ -72,11 +71,11 @@ func refreshTokens(
|
||||
}[aT.TTL]
|
||||
// issue new tokens for the user
|
||||
user := contexts.GetUser(r.Context())
|
||||
err = cookies.SetTokenCookies(w, r, config, user.User, true, rememberMe)
|
||||
err = cookies.SetTokenCookies(w, r, config, tokenGen, user.User, true, rememberMe)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cookies.SetTokenCookies")
|
||||
}
|
||||
err = revokeTokenPair(ctx, tx, aT, rT)
|
||||
err = revokeTokenPair(tx, aT, rT)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "revokeTokenPair")
|
||||
}
|
||||
@@ -86,12 +85,13 @@ func refreshTokens(
|
||||
|
||||
// Validate the provided password
|
||||
func validatePassword(
|
||||
tx *sql.Tx,
|
||||
r *http.Request,
|
||||
) error {
|
||||
r.ParseForm()
|
||||
password := r.FormValue("password")
|
||||
user := contexts.GetUser(r.Context())
|
||||
err := user.CheckPassword(password)
|
||||
err := user.CheckPassword(tx, password)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "user.CheckPassword")
|
||||
}
|
||||
@@ -100,9 +100,10 @@ func validatePassword(
|
||||
|
||||
// Handle request to reauthenticate (i.e. make token fresh again)
|
||||
func Reauthenticate(
|
||||
logger *zerolog.Logger,
|
||||
logger *hlog.Logger,
|
||||
config *config.Config,
|
||||
conn *db.SafeConn,
|
||||
conn *sql.DB,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -110,22 +111,21 @@ func Reauthenticate(
|
||||
defer cancel()
|
||||
|
||||
// Start the transaction
|
||||
tx, err := conn.Begin(ctx)
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Failed to refresh user tokens")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
logger.Error().Err(err).Msg("Failed to start transaction")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = validatePassword(r)
|
||||
defer tx.Rollback()
|
||||
err = validatePassword(tx, r)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
w.WriteHeader(445)
|
||||
form.ConfirmPassword("Incorrect password").Render(r.Context(), w)
|
||||
return
|
||||
}
|
||||
err = refreshTokens(config, ctx, tx, w, r)
|
||||
err = refreshTokens(config, tokenGen, tx, w, r)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
logger.Error().Err(err).Msg("Failed to refresh user tokens")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -10,23 +11,23 @@ import (
|
||||
"projectreshoot/internal/view/page"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/cookies"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func validateRegistration(
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tx *sql.Tx,
|
||||
r *http.Request,
|
||||
) (*models.User, error) {
|
||||
formUsername := r.FormValue("username")
|
||||
formPassword := r.FormValue("password")
|
||||
formConfirmPassword := r.FormValue("confirm-password")
|
||||
unique, err := models.CheckUsernameUnique(ctx, tx, formUsername)
|
||||
unique, err := models.CheckUsernameUnique(tx, formUsername)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.CheckUsernameUnique")
|
||||
return nil, errors.Wrap(err, "models.CheckUsernameUnique")
|
||||
}
|
||||
if !unique {
|
||||
return nil, errors.New("Username is taken")
|
||||
@@ -37,9 +38,9 @@ func validateRegistration(
|
||||
if len(formPassword) > 72 {
|
||||
return nil, errors.New("Password exceeds maximum length of 72 bytes")
|
||||
}
|
||||
user, err := models.CreateNewUser(ctx, tx, formUsername, formPassword)
|
||||
user, err := models.CreateNewUser(tx, formUsername, formPassword)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.CreateNewUser")
|
||||
return nil, errors.Wrap(err, "models.CreateNewUser")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
@@ -47,8 +48,9 @@ func validateRegistration(
|
||||
|
||||
func RegisterRequest(
|
||||
config *config.Config,
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
logger *hlog.Logger,
|
||||
conn *sql.DB,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -56,14 +58,14 @@ func RegisterRequest(
|
||||
defer cancel()
|
||||
|
||||
// Start the transaction
|
||||
tx, err := conn.Begin(ctx)
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("Failed to set token cookies")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
r.ParseForm()
|
||||
user, err := validateRegistration(ctx, tx, r)
|
||||
user, err := validateRegistration(tx, r)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
if err.Error() != "Username is taken" &&
|
||||
@@ -78,7 +80,7 @@ func RegisterRequest(
|
||||
}
|
||||
|
||||
rememberMe := checkRememberMe(r)
|
||||
err = cookies.SetTokenCookies(w, r, config, user, true, rememberMe)
|
||||
err = cookies.SetTokenCookies(w, r, config, tokenGen, user, true, rememberMe)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"projectreshoot/internal/handler"
|
||||
"projectreshoot/internal/middleware"
|
||||
"projectreshoot/internal/view/page"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
)
|
||||
|
||||
// Add all the handled routes to the mux
|
||||
func addRoutes(
|
||||
mux *http.ServeMux,
|
||||
logger *zerolog.Logger,
|
||||
logger *hlog.Logger,
|
||||
config *config.Config,
|
||||
conn *db.SafeConn,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
conn *sql.DB,
|
||||
staticFS *http.FileSystem,
|
||||
) {
|
||||
route := mux.Handle
|
||||
@@ -39,17 +41,17 @@ func addRoutes(
|
||||
|
||||
// Login page and handlers
|
||||
route("GET /login", loggedOut(handler.LoginPage(config.TrustedHost)))
|
||||
route("POST /login", loggedOut(handler.LoginRequest(config, logger, conn)))
|
||||
route("POST /login", loggedOut(handler.LoginRequest(config, logger, conn, tokenGen)))
|
||||
|
||||
// Register page and handlers
|
||||
route("GET /register", loggedOut(handler.RegisterPage(config.TrustedHost)))
|
||||
route("POST /register", loggedOut(handler.RegisterRequest(config, logger, conn)))
|
||||
route("POST /register", loggedOut(handler.RegisterRequest(config, tokenGen, logger, conn)))
|
||||
|
||||
// Logout
|
||||
route("POST /logout", handler.Logout(config, logger, conn))
|
||||
route("POST /logout", handler.Logout(conn, tokenGen, logger))
|
||||
|
||||
// Reauthentication request
|
||||
route("POST /reauthenticate", loggedIn(handler.Reauthenticate(logger, config, conn)))
|
||||
route("POST /reauthenticate", loggedIn(handler.Reauthenticate(logger, config, conn, tokenGen)))
|
||||
|
||||
// Profile page
|
||||
route("GET /profile", loggedIn(handler.ProfilePage()))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -8,20 +9,21 @@ import (
|
||||
|
||||
"projectreshoot/internal/middleware"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
)
|
||||
|
||||
func NewServer(
|
||||
config *config.Config,
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
logger *hlog.Logger,
|
||||
conn *sql.DB,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
staticFS *fs.FS,
|
||||
maint *uint32,
|
||||
) *http.Server {
|
||||
fs := http.FS(*staticFS)
|
||||
srv := createServer(config, logger, conn, &fs, maint)
|
||||
srv := createServer(config, logger, conn, tokenGen, &fs, maint)
|
||||
httpServer := &http.Server{
|
||||
Addr: net.JoinHostPort(config.Host, config.Port),
|
||||
Handler: srv,
|
||||
@@ -35,8 +37,9 @@ func NewServer(
|
||||
// Returns a new http.Handler with all the routes and middleware added
|
||||
func createServer(
|
||||
config *config.Config,
|
||||
logger *zerolog.Logger,
|
||||
conn *db.SafeConn,
|
||||
logger *hlog.Logger,
|
||||
conn *sql.DB,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
staticFS *http.FileSystem,
|
||||
maint *uint32,
|
||||
) http.Handler {
|
||||
@@ -45,6 +48,7 @@ func createServer(
|
||||
mux,
|
||||
logger,
|
||||
config,
|
||||
tokenGen,
|
||||
conn,
|
||||
staticFS,
|
||||
)
|
||||
@@ -52,7 +56,7 @@ func createServer(
|
||||
// Add middleware here, must be added in reverse order of execution
|
||||
// i.e. First in list will get executed last during the request handling
|
||||
handler = middleware.Logging(logger, handler)
|
||||
handler = middleware.Authentication(logger, config, conn, handler, maint)
|
||||
handler = middleware.Authentication(logger, config, conn, tokenGen, handler, maint)
|
||||
|
||||
// Gzip
|
||||
handler = middleware.Gzip(handler, config.GZIP)
|
||||
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -11,25 +12,24 @@ import (
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/contexts"
|
||||
"projectreshoot/pkg/cookies"
|
||||
"projectreshoot/pkg/db"
|
||||
"projectreshoot/pkg/jwt"
|
||||
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Attempt to use a valid refresh token to generate a new token pair
|
||||
func refreshAuthTokens(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
tx *sql.Tx,
|
||||
w http.ResponseWriter,
|
||||
req *http.Request,
|
||||
ref *jwt.RefreshToken,
|
||||
) (*models.User, error) {
|
||||
user, err := ref.GetUser(ctx, tx)
|
||||
user, err := models.GetUserFromID(tx, ref.SUB)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "ref.GetUser")
|
||||
return nil, errors.Wrap(err, "models.GetUser")
|
||||
}
|
||||
|
||||
rememberMe := map[string]bool{
|
||||
@@ -38,14 +38,14 @@ func refreshAuthTokens(
|
||||
}[ref.TTL]
|
||||
|
||||
// Set fresh to true because new tokens coming from refresh request
|
||||
err = cookies.SetTokenCookies(w, req, config, user, false, rememberMe)
|
||||
err = cookies.SetTokenCookies(w, req, config, tokenGen, user, false, rememberMe)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cookies.SetTokenCookies")
|
||||
}
|
||||
// New tokens sent, revoke the used refresh token
|
||||
err = jwt.RevokeToken(ctx, tx, ref)
|
||||
err = ref.Revoke(tx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "jwt.RevokeToken")
|
||||
return nil, errors.Wrap(err, "ref.Revoke")
|
||||
}
|
||||
// Return the authorized user
|
||||
return user, nil
|
||||
@@ -54,23 +54,26 @@ func refreshAuthTokens(
|
||||
// Check the cookies for token strings and attempt to authenticate them
|
||||
func getAuthenticatedUser(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
tx *sql.Tx,
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
) (*contexts.AuthenticatedUser, error) {
|
||||
// Get token strings from cookies
|
||||
atStr, rtStr := cookies.GetTokenStrings(r)
|
||||
if atStr == "" && rtStr == "" {
|
||||
return nil, errors.New("No token strings provided")
|
||||
}
|
||||
// Attempt to parse the access token
|
||||
aT, err := jwt.ParseAccessToken(config, ctx, tx, atStr)
|
||||
aT, err := tokenGen.ValidateAccess(tx, atStr)
|
||||
if err != nil {
|
||||
// Access token invalid, attempt to parse refresh token
|
||||
rT, err := jwt.ParseRefreshToken(config, ctx, tx, rtStr)
|
||||
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "jwt.ParseRefreshToken")
|
||||
return nil, errors.Wrap(err, "tokenGen.ValidateRefresh")
|
||||
}
|
||||
// Refresh token valid, attempt to get a new token pair
|
||||
user, err := refreshAuthTokens(config, ctx, tx, w, r, rT)
|
||||
user, err := refreshAuthTokens(config, tokenGen, tx, w, r, rT)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "refreshAuthTokens")
|
||||
}
|
||||
@@ -82,9 +85,9 @@ func getAuthenticatedUser(
|
||||
return &authUser, nil
|
||||
}
|
||||
// Access token valid
|
||||
user, err := aT.GetUser(ctx, tx)
|
||||
user, err := models.GetUserFromID(tx, aT.SUB)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "aT.GetUser")
|
||||
return nil, errors.Wrap(err, "models.GetUser")
|
||||
}
|
||||
authUser := contexts.AuthenticatedUser{
|
||||
User: user,
|
||||
@@ -96,9 +99,10 @@ func getAuthenticatedUser(
|
||||
// Attempt to authenticate the user and add their account details
|
||||
// to the request context
|
||||
func Authentication(
|
||||
logger *zerolog.Logger,
|
||||
logger *hlog.Logger,
|
||||
config *config.Config,
|
||||
conn *db.SafeConn,
|
||||
conn *sql.DB,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
next http.Handler,
|
||||
maint *uint32,
|
||||
) http.Handler {
|
||||
@@ -115,7 +119,7 @@ func Authentication(
|
||||
}
|
||||
|
||||
// Start the transaction
|
||||
tx, err := conn.Begin(ctx)
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
// Failed to start transaction, skip auth
|
||||
logger.Warn().Err(err).
|
||||
@@ -123,7 +127,7 @@ func Authentication(
|
||||
handler.ErrorPage(http.StatusServiceUnavailable, w, r)
|
||||
return
|
||||
}
|
||||
user, err := getAuthenticatedUser(config, ctx, tx, w, r)
|
||||
user, err := getAuthenticatedUser(config, tokenGen, tx, w, r)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
// User auth failed, delete the cookies to avoid repeat requests
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"projectreshoot/pkg/contexts"
|
||||
"projectreshoot/pkg/db"
|
||||
"projectreshoot/pkg/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuthenticationMiddleware(t *testing.T) {
|
||||
cfg, err := tests.TestConfig()
|
||||
require.NoError(t, err)
|
||||
logger := tests.NilLogger()
|
||||
ver, err := strconv.ParseInt(cfg.DBName, 10, 0)
|
||||
require.NoError(t, err)
|
||||
wconn, rconn, err := tests.SetupTestDB(ver)
|
||||
require.NoError(t, err)
|
||||
sconn := db.MakeSafe(wconn, rconn, logger)
|
||||
defer sconn.Close()
|
||||
|
||||
// Handler to check outcome of Authentication middleware
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := contexts.GetUser(r.Context())
|
||||
if user == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(strconv.Itoa(0)))
|
||||
return
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(strconv.Itoa(user.ID)))
|
||||
}
|
||||
})
|
||||
var maint uint32
|
||||
atomic.StoreUint32(&maint, 0)
|
||||
// Add the middleware and create the server
|
||||
authHandler := Authentication(logger, cfg, sconn, testHandler, &maint)
|
||||
require.NoError(t, err)
|
||||
server := httptest.NewServer(authHandler)
|
||||
defer server.Close()
|
||||
|
||||
tokens := getTokens()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id int
|
||||
accessToken string
|
||||
refreshToken string
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "Valid Access Token (Fresh)",
|
||||
id: 1,
|
||||
accessToken: tokens["accessFresh"],
|
||||
refreshToken: "",
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Valid Access Token (Unfresh)",
|
||||
id: 1,
|
||||
accessToken: tokens["accessUnfresh"],
|
||||
refreshToken: tokens["refreshExpired"],
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Valid Refresh Token (Triggers Refresh)",
|
||||
id: 1,
|
||||
accessToken: tokens["accessExpired"],
|
||||
refreshToken: tokens["refreshValid"],
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Both tokens expired",
|
||||
accessToken: tokens["accessExpired"],
|
||||
refreshToken: tokens["refreshExpired"],
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "Access token revoked",
|
||||
accessToken: tokens["accessRevoked"],
|
||||
refreshToken: "",
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "Refresh token revoked",
|
||||
accessToken: "",
|
||||
refreshToken: tokens["refreshRevoked"],
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "Invalid Tokens",
|
||||
accessToken: tokens["invalid"],
|
||||
refreshToken: tokens["invalid"],
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "No Tokens",
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &http.Client{}
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||
|
||||
// Add cookies if provided
|
||||
if tt.accessToken != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "access", Value: tt.accessToken})
|
||||
}
|
||||
if tt.refreshToken != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "refresh", Value: tt.refreshToken})
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedCode, resp.StatusCode)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, strconv.Itoa(tt.id), string(body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// get the tokens to test with
|
||||
func getTokens() map[string]string {
|
||||
tokens := map[string]string{
|
||||
"accessFresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ4OTU2NzIyMTAsImZyZXNoIjo0ODk1NjcyMjEwLCJpYXQiOjE3Mzk2NzIyMTAsImlzcyI6IjEyNy4wLjAuMSIsImp0aSI6ImE4Njk2YWM4LTg3OWMtNDdkNC1iZWM2LTRlY2Y4MTRiZThiZiIsInNjb3BlIjoiYWNjZXNzIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.6nAquDY0JBLPdaJ9q_sMpKj1ISG4Vt2U05J57aoPue8",
|
||||
"accessUnfresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjMzMjk5Njc1NjcxLCJmcmVzaCI6MTczOTY3NTY3MSwiaWF0IjoxNzM5Njc1NjcxLCJpc3MiOiIxMjcuMC4wLjEiLCJqdGkiOiJjOGNhZmFjNy0yODkzLTQzNzMtOTI4ZS03MGUwODJkYmM2MGIiLCJzY29wZSI6ImFjY2VzcyIsInN1YiI6MSwidHRsIjoic2Vzc2lvbiJ9.plWQVFwHlhXUYI5utS7ny1JfXjJSFrigkq-PnTHD5VY",
|
||||
"accessExpired": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzk2NzIyNDgsImZyZXNoIjoxNzM5NjcyMjQ4LCJpYXQiOjE3Mzk2NzIyNDgsImlzcyI6IjEyNy4wLjAuMSIsImp0aSI6IjgxYzA1YzBjLTJhOGItNGQ2MC04Yzc4LWY2ZTQxODYxZDFmNCIsInNjb3BlIjoiYWNjZXNzIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.iI1f17kKTuFDEMEYltJRIwRYgYQ-_nF9Wsn0KR6x77Q",
|
||||
"refreshValid": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ4OTU2NzE5MjIsImlhdCI6MTczOTY3MTkyMiwiaXNzIjoiMTI3LjAuMC4xIiwianRpIjoiZTUxMTY3ZWEtNDA3OS00ZTczLTkzZDQtNTgwZDMzODRjZDU4Iiwic2NvcGUiOiJyZWZyZXNoIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.tvtqQ8Z4WrYWHHb0MaEPdsU2FT2KLRE1zHOv3ipoFyc",
|
||||
"refreshExpired": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzk2NzIyNDgsImlhdCI6MTczOTY3MjI0OCwiaXNzIjoiMTI3LjAuMC4xIiwianRpIjoiZTg5YTc5MTYtZGEzYi00YmJhLWI3ZDMtOWI1N2ViNjRhMmU0Iiwic2NvcGUiOiJyZWZyZXNoIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.rH_fytC7Duxo598xacu820pQKF9ELbG8674h_bK_c4I",
|
||||
"accessRevoked": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ4OTU2NzE5MjIsImZyZXNoIjoxNzM5NjcxOTIyLCJpYXQiOjE3Mzk2NzE5MjIsImlzcyI6IjEyNy4wLjAuMSIsImp0aSI6IjBhNmIzMzhlLTkzMGEtNDNmZS04ZjcwLTFhNmRhZWQyNTZmYSIsInNjb3BlIjoiYWNjZXNzIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.mZLuCp9amcm2_CqYvbHPlk86nfiuy_Or8TlntUCw4Qs",
|
||||
"refreshRevoked": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjMzMjk5Njc1NjcxLCJpYXQiOjE3Mzk2NzU2NzEsImlzcyI6IjEyNy4wLjAuMSIsImp0aSI6ImI3ZmE1MWRjLTg1MzItNDJlMS04NzU2LTVkMjViZmIyMDAzYSIsInNjb3BlIjoicmVmcmVzaCIsInN1YiI6MSwidHRsIjoic2Vzc2lvbiJ9.5Q9yDZN5FubfCWHclUUZEkJPOUHcOEpVpgcUK-ameHo",
|
||||
"invalid": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODUxNDA5ODQsImlhdCI6MTQ4NTEzNzM4NCwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIyOWFjMGMxOC0wYjRhLTQyY2YtODJmYy0wM2Q1NzAzMThhMWQiLCJhcHBsaWNhdGlvbklkIjoiNzkxMDM3MzQtOTdhYi00ZDFhLWFmMzctZTAwNmQwNWQyOTUyIiwicm9sZXMiOltdfQ.Mp0Pcwsz5VECK11Kf2ZZNF_SMKu5CgBeLN9ZOP04kZo",
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"projectreshoot/pkg/contexts"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
)
|
||||
|
||||
// Wraps the http.ResponseWriter, adding a statusCode field
|
||||
@@ -22,7 +22,7 @@ func (w *wrappedWriter) WriteHeader(statusCode int) {
|
||||
}
|
||||
|
||||
// Middleware to add logs to console with details of the request
|
||||
func Logging(logger *zerolog.Logger, next http.Handler) http.Handler {
|
||||
func Logging(logger *hlog.Logger, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/static/css/output.css" ||
|
||||
r.URL.Path == "/static/favicon.ico" {
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"projectreshoot/pkg/db"
|
||||
"projectreshoot/pkg/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPageLoginRequired(t *testing.T) {
|
||||
cfg, err := tests.TestConfig()
|
||||
require.NoError(t, err)
|
||||
logger := tests.NilLogger()
|
||||
ver, err := strconv.ParseInt(cfg.DBName, 10, 0)
|
||||
require.NoError(t, err)
|
||||
wconn, rconn, err := tests.SetupTestDB(ver)
|
||||
require.NoError(t, err)
|
||||
sconn := db.MakeSafe(wconn, rconn, logger)
|
||||
defer sconn.Close()
|
||||
|
||||
// Handler to check outcome of Authentication middleware
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
var maint uint32
|
||||
atomic.StoreUint32(&maint, 0)
|
||||
// Add the middleware and create the server
|
||||
loginRequiredHandler := LoginReq(testHandler)
|
||||
authHandler := Authentication(logger, cfg, sconn, loginRequiredHandler, &maint)
|
||||
server := httptest.NewServer(authHandler)
|
||||
defer server.Close()
|
||||
|
||||
tokens := getTokens()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "Valid Login",
|
||||
accessToken: tokens["accessFresh"],
|
||||
refreshToken: "",
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Expired login",
|
||||
accessToken: tokens["accessExpired"],
|
||||
refreshToken: tokens["refreshExpired"],
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "No login",
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &http.Client{}
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||
|
||||
// Add cookies if provided
|
||||
if tt.accessToken != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "access", Value: tt.accessToken})
|
||||
}
|
||||
if tt.refreshToken != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "refresh", Value: tt.refreshToken})
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedCode, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"projectreshoot/pkg/db"
|
||||
"projectreshoot/pkg/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReauthRequired(t *testing.T) {
|
||||
cfg, err := tests.TestConfig()
|
||||
require.NoError(t, err)
|
||||
logger := tests.NilLogger()
|
||||
ver, err := strconv.ParseInt(cfg.DBName, 10, 0)
|
||||
require.NoError(t, err)
|
||||
wconn, rconn, err := tests.SetupTestDB(ver)
|
||||
require.NoError(t, err)
|
||||
sconn := db.MakeSafe(wconn, rconn, logger)
|
||||
defer sconn.Close()
|
||||
|
||||
// Handler to check outcome of Authentication middleware
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
var maint uint32
|
||||
atomic.StoreUint32(&maint, 0)
|
||||
// Add the middleware and create the server
|
||||
reauthRequiredHandler := FreshReq(testHandler)
|
||||
loginRequiredHandler := LoginReq(reauthRequiredHandler)
|
||||
authHandler := Authentication(logger, cfg, sconn, loginRequiredHandler, &maint)
|
||||
server := httptest.NewServer(authHandler)
|
||||
defer server.Close()
|
||||
|
||||
tokens := getTokens()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
accessToken string
|
||||
refreshToken string
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "Fresh Login",
|
||||
accessToken: tokens["accessFresh"],
|
||||
refreshToken: "",
|
||||
expectedCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Unfresh Login",
|
||||
accessToken: tokens["accessUnfresh"],
|
||||
refreshToken: "",
|
||||
expectedCode: 444,
|
||||
},
|
||||
{
|
||||
name: "Expired login",
|
||||
accessToken: tokens["accessExpired"],
|
||||
refreshToken: tokens["refreshExpired"],
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
name: "No login",
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
expectedCode: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &http.Client{}
|
||||
|
||||
req, _ := http.NewRequest(http.MethodGet, server.URL, nil)
|
||||
|
||||
// Add cookies if provided
|
||||
if tt.accessToken != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "access", Value: tt.accessToken})
|
||||
}
|
||||
if tt.refreshToken != "" {
|
||||
req.AddCookie(&http.Cookie{Name: "refresh", Value: tt.refreshToken})
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedCode, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,31 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"projectreshoot/pkg/db"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int // Integer ID (index primary key)
|
||||
Username string // Username (unique)
|
||||
Password_hash string // Bcrypt password hash
|
||||
Created_at int64 // Epoch timestamp when the user was added to the database
|
||||
Bio string // Short byline set by the user
|
||||
ID int // Integer ID (index primary key)
|
||||
Username string // Username (unique)
|
||||
Created_at int64 // Epoch timestamp when the user was added to the database
|
||||
Bio string // Short byline set by the user
|
||||
}
|
||||
|
||||
// Uses bcrypt to set the users Password_hash from the given password
|
||||
func (user *User) SetPassword(ctx context.Context, tx *db.SafeWTX, password string) error {
|
||||
func (user *User) SetPassword(
|
||||
tx *sql.Tx,
|
||||
password string,
|
||||
) error {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "bcrypt.GenerateFromPassword")
|
||||
}
|
||||
user.Password_hash = string(hashedPassword)
|
||||
newPassword := string(hashedPassword)
|
||||
query := `UPDATE users SET password_hash = ? WHERE id = ?`
|
||||
_, err = tx.Exec(ctx, query, user.Password_hash, user.ID)
|
||||
_, err = tx.Exec(query, newPassword, user.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Exec")
|
||||
}
|
||||
@@ -32,8 +33,15 @@ func (user *User) SetPassword(ctx context.Context, tx *db.SafeWTX, password stri
|
||||
}
|
||||
|
||||
// Uses bcrypt to check if the given password matches the users Password_hash
|
||||
func (user *User) CheckPassword(password string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password_hash), []byte(password))
|
||||
func (user *User) CheckPassword(tx *sql.Tx, password string) error {
|
||||
query := `SELECT password_hash FROM users WHERE id = ? LIMIT 1`
|
||||
row := tx.QueryRow(query, user.ID)
|
||||
hashedPassword := ""
|
||||
err := row.Scan(&hashedPassword)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "row.Scan")
|
||||
}
|
||||
err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "bcrypt.CompareHashAndPassword")
|
||||
}
|
||||
@@ -41,9 +49,9 @@ func (user *User) CheckPassword(password string) error {
|
||||
}
|
||||
|
||||
// Change the user's username
|
||||
func (user *User) ChangeUsername(ctx context.Context, tx *db.SafeWTX, newUsername string) error {
|
||||
func (user *User) ChangeUsername(tx *sql.Tx, newUsername string) error {
|
||||
query := `UPDATE users SET username = ? WHERE id = ?`
|
||||
_, err := tx.Exec(ctx, query, newUsername, user.ID)
|
||||
_, err := tx.Exec(query, newUsername, user.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Exec")
|
||||
}
|
||||
@@ -51,9 +59,9 @@ func (user *User) ChangeUsername(ctx context.Context, tx *db.SafeWTX, newUsernam
|
||||
}
|
||||
|
||||
// Change the user's bio
|
||||
func (user *User) ChangeBio(ctx context.Context, tx *db.SafeWTX, newBio string) error {
|
||||
func (user *User) ChangeBio(tx *sql.Tx, newBio string) error {
|
||||
query := `UPDATE users SET bio = ? WHERE id = ?`
|
||||
_, err := tx.Exec(ctx, query, newBio, user.ID)
|
||||
_, err := tx.Exec(query, newBio, user.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Exec")
|
||||
}
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Creates a new user in the database and returns a pointer
|
||||
func CreateNewUser(
|
||||
ctx context.Context,
|
||||
tx *db.SafeWTX,
|
||||
tx *sql.Tx,
|
||||
username string,
|
||||
password string,
|
||||
) (*User, error) {
|
||||
query := `INSERT INTO users (username) VALUES (?)`
|
||||
_, err := tx.Exec(ctx, query, username)
|
||||
_, err := tx.Exec(query, username)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.Exec")
|
||||
}
|
||||
user, err := GetUserFromUsername(ctx, tx, username)
|
||||
user, err := GetUserFromUsername(tx, username)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "GetUserFromUsername")
|
||||
}
|
||||
err = user.SetPassword(ctx, tx, password)
|
||||
err = user.SetPassword(tx, password)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "user.SetPassword")
|
||||
}
|
||||
@@ -34,23 +31,21 @@ func CreateNewUser(
|
||||
|
||||
// Fetches data from the users table using "WHERE column = 'value'"
|
||||
func fetchUserData(
|
||||
ctx context.Context,
|
||||
tx db.SafeTX,
|
||||
tx *sql.Tx,
|
||||
column string,
|
||||
value interface{},
|
||||
value any,
|
||||
) (*sql.Rows, error) {
|
||||
query := fmt.Sprintf(
|
||||
`SELECT
|
||||
id,
|
||||
username,
|
||||
password_hash,
|
||||
created_at,
|
||||
bio
|
||||
FROM users
|
||||
WHERE %s = ? COLLATE NOCASE LIMIT 1`,
|
||||
column,
|
||||
)
|
||||
rows, err := tx.Query(ctx, query, value)
|
||||
rows, err := tx.Query(query, value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.Query")
|
||||
}
|
||||
@@ -66,7 +61,6 @@ func scanUserRow(user *User, rows *sql.Rows) error {
|
||||
err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.Password_hash,
|
||||
&user.Created_at,
|
||||
&user.Bio,
|
||||
)
|
||||
@@ -78,8 +72,8 @@ func scanUserRow(user *User, rows *sql.Rows) error {
|
||||
|
||||
// Queries the database for a user matching the given username.
|
||||
// Query is case insensitive
|
||||
func GetUserFromUsername(ctx context.Context, tx db.SafeTX, username string) (*User, error) {
|
||||
rows, err := fetchUserData(ctx, tx, "username", username)
|
||||
func GetUserFromUsername(tx *sql.Tx, username string) (*User, error) {
|
||||
rows, err := fetchUserData(tx, "username", username)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "fetchUserData")
|
||||
}
|
||||
@@ -93,8 +87,8 @@ func GetUserFromUsername(ctx context.Context, tx db.SafeTX, username string) (*U
|
||||
}
|
||||
|
||||
// Queries the database for a user matching the given ID.
|
||||
func GetUserFromID(ctx context.Context, tx db.SafeTX, id int) (*User, error) {
|
||||
rows, err := fetchUserData(ctx, tx, "id", id)
|
||||
func GetUserFromID(tx *sql.Tx, id int) (*User, error) {
|
||||
rows, err := fetchUserData(tx, "id", id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "fetchUserData")
|
||||
}
|
||||
@@ -108,9 +102,9 @@ func GetUserFromID(ctx context.Context, tx db.SafeTX, id int) (*User, error) {
|
||||
}
|
||||
|
||||
// Checks if the given username is unique. Returns true if not taken
|
||||
func CheckUsernameUnique(ctx context.Context, tx db.SafeTX, username string) (bool, error) {
|
||||
func CheckUsernameUnique(tx *sql.Tx, username string) (bool, error) {
|
||||
query := `SELECT 1 FROM users WHERE username = ? COLLATE NOCASE LIMIT 1`
|
||||
rows, err := tx.Query(ctx, query, username)
|
||||
rows, err := tx.Query(query, username)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "tx.Query")
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package account
|
||||
|
||||
//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 "projectreshoot/pkg/contexts"
|
||||
|
||||
func ChangeBio(err string, bio 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)
|
||||
|
||||
user := contexts.GetUser(ctx)
|
||||
if bio == "" {
|
||||
bio = user.Bio
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form hx-post=\"/change-bio\" hx-swap=\"outerHTML\" class=\"w-[90%] mx-auto mt-5\" x-data=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSFuncCall("bioComponent", bio, user.Bio, err).CallInline)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/account/changebio.templ`, Line: 16, Col: 74}
|
||||
}
|
||||
_, 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, "\"><script>\n function bioComponent(newBio, oldBio, err) {\n return {\n bio: newBio,\n initialBio: oldBio, \n err: err,\n bioLenText: '', \n updateTextArea() {\n this.$nextTick(() => {\n if (this.$refs.bio) {\n this.$refs.bio.style.height = 'auto';\n this.$refs.bio.style.height = `\n ${this.$refs.bio.scrollHeight+20}px`;\n };\n this.bioLenText = `${this.bio.length}/128`;\n });\n },\n resetBio() {\n this.bio = this.initialBio;\n this.err = \"\",\n this.updateTextArea();\n },\n init() {\n this.$nextTick(() => {\n // this timeout makes sure the textarea resizes on \n // page render correctly. seems 20ms is the sweet\n // spot between a noticable delay and not working\n setTimeout(() => {\n this.updateTextArea();\n }, 20);\n });\n }\n };\n }\n </script><div class=\"flex flex-col\"><div class=\"flex flex-col sm:flex-row sm:items-center relative\"><label for=\"bio\" class=\"text-lg w-20\">Bio</label><div class=\"relative sm:ml-5 ml-0 w-fit\"><textarea type=\"text\" id=\"bio\" name=\"bio\" class=\"py-1 px-4 rounded-lg text-md\n bg-surface0 border border-surface2 w-60\n disabled:opacity-50 disabled:pointer-events-none\" required aria-describedby=\"bio-error\" x-model=\"bio\" x-ref=\"bio\" @input=\"updateTextArea()\" maxlength=\"128\"></textarea> <span class=\"absolute right-0 pr-2 bottom-0 pb-2 text-overlay2\" x-text=\"bioLenText\"></span></div></div><div class=\"mt-2 sm:ml-25\"><button class=\"rounded-lg bg-blue py-1 px-2 text-mantle \n hover:cursor-pointer hover:bg-blue/75 transition\" x-cloak x-show=\"bio !== initialBio\" x-transition.opacity.duration.500ms>Update</button> <button class=\"rounded-lg bg-overlay0 py-1 px-2 text-mantle\n hover:cursor-pointer hover:bg-surface2 transition\" type=\"button\" href=\"#\" x-cloak x-show=\"bio !== initialBio\" x-transition.opacity.duration.500ms @click=\"resetBio()\">Cancel</button></div></div><p class=\"block text-red sm:ml-26 mt-1 transition\" x-cloak x-show=\"err\" x-text=\"err\"></p></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,55 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package account
|
||||
|
||||
//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"
|
||||
|
||||
func ChangePassword(err 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form hx-post=\"/change-password\" hx-swap=\"outerHTML\" class=\"w-[90%] mx-auto mt-5\" x-data=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSFuncCall(
|
||||
"passwordComponent", err,
|
||||
).CallInline)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/account/changepassword.templ`, Line: 10, Col: 32}
|
||||
}
|
||||
_, 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, "\"><script>\n function passwordComponent(err) {\n return {\n password: \"\",\n confirmPassword: \"\",\n err: err,\n reset() {\n this.err = \"\";\n this.password = \"\";\n this.confirmPassword = \"\";\n },\n };\n }\n </script><div class=\"flex flex-col\"><div class=\"flex flex-col sm:flex-row sm:items-center relative w-fit\"><label for=\"password\" class=\"text-lg w-40\">New Password</label> <input type=\"password\" id=\"password\" name=\"password\" class=\"py-1 px-4 rounded-lg text-md\n bg-surface0 border border-surface2 w-50 sm:ml-5\n disabled:opacity-50 ml-0 disabled:pointer-events-none\" required aria-describedby=\"password-error\" x-model=\"password\"><div class=\"absolute inset-y-0 end-0 pt-9\n pointer-events-none sm:pt-2 pe-2\" x-show=\"err\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 \n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 \n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div></div><div class=\"flex flex-col sm:flex-row sm:items-center relative mt-2 w-fit\"><label for=\"confirm-password\" class=\"text-lg w-40\">Confirm Password</label> <input type=\"password\" id=\"confirm-password\" name=\"confirm-password\" class=\"py-1 px-4 rounded-lg text-md\n bg-surface0 border border-surface2 w-50 sm:ml-5\n disabled:opacity-50 ml-0 disabled:pointer-events-none\" required aria-describedby=\"password-error\" x-model=\"confirmPassword\"><div class=\"absolute inset-y-0 pe-2 end-0 pt-9\n pointer-events-none sm:pt-2\" x-show=\"err\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 \n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 \n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div></div><div class=\"mt-2 sm:ml-43\"><button class=\"rounded-lg bg-blue py-1 px-2 text-mantle sm:ml-2\n hover:cursor-pointer hover:bg-blue/75 transition\" x-cloak x-show=\"password !== '' || confirmPassword !== ''\" x-transition.opacity.duration.500ms>Update</button> <button class=\"rounded-lg bg-overlay0 py-1 px-2 text-mantle\n hover:cursor-pointer hover:bg-surface2 transition\" type=\"button\" x-cloak x-show=\"password !== '' || confirmPassword !== ''\" x-transition.opacity.duration.500ms @click=\"reset()\">Cancel</button></div></div><p class=\"block text-red sm:ml-45 mt-1 transition\" x-cloak x-show=\"err\" x-text=\"err\"></p></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,62 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package account
|
||||
|
||||
//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 "projectreshoot/pkg/contexts"
|
||||
|
||||
func ChangeUsername(err string, username 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)
|
||||
|
||||
user := contexts.GetUser(ctx)
|
||||
if username == "" {
|
||||
username = user.Username
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form hx-post=\"/change-username\" hx-swap=\"outerHTML\" class=\"w-[90%] mx-auto mt-5\" x-data=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSFuncCall(
|
||||
"usernameComponent", username, user.Username, err,
|
||||
).CallInline)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/account/changeusername.templ`, Line: 18, Col: 32}
|
||||
}
|
||||
_, 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, "\"><script>\n function usernameComponent(newUsername, oldUsername, err) {\n return {\n username: newUsername,\n initialUsername: oldUsername, \n err: err,\n resetUsername() {\n this.username = this.initialUsername;\n this.err = \"\";\n },\n };\n }\n </script><div class=\"flex flex-col sm:flex-row\"><div class=\"flex flex-col sm:flex-row sm:items-center relative\"><label for=\"username\" class=\"text-lg w-20\">Username</label> <input type=\"text\" id=\"username\" name=\"username\" class=\"py-1 px-4 rounded-lg text-md\n bg-surface0 border border-surface2 w-50 sm:ml-5\n disabled:opacity-50 ml-0 disabled:pointer-events-none\" required aria-describedby=\"username-error\" x-model=\"username\"><div class=\"absolute inset-y-0 sm:start-68 start-43 pt-9\n pointer-events-none sm:pt-2\" x-show=\"err\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 \n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 \n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div></div><div class=\"mt-2 sm:mt-0\"><button class=\"rounded-lg bg-blue py-1 px-2 text-mantle sm:ml-2\n hover:cursor-pointer hover:bg-blue/75 transition\" x-cloak x-show=\"username !== initialUsername\" x-transition.opacity.duration.500ms>Update</button> <button class=\"rounded-lg bg-overlay0 py-1 px-2 text-mantle\n hover:cursor-pointer hover:bg-surface2 transition\" type=\"button\" href=\"#\" x-cloak x-show=\"username !== initialUsername\" x-transition.opacity.duration.500ms @click=\"resetUsername()\">Cancel</button></div></div><p class=\"block text-red sm:ml-26 mt-1 transition\" x-cloak x-show=\"err\" x-text=\"err\"></p></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,77 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package account
|
||||
|
||||
//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"
|
||||
|
||||
func AccountContainer(subpage 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"account-container\" class=\"flex max-w-200 min-h-100 mx-5 md:mx-auto bg-mantle mt-5 rounded-xl\" x-data=\"{big:window.innerWidth >=768, open:false}\" @resize.window=\"big = window.innerWidth >= 768\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = SelectMenu(subpage).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"mt-5 w-full md:ml-[200px] ml-[40px] transition-all duration-300\"><div class=\"pl-5 text-2xl text-subtext1 border-b \n border-overlay0 w-[90%] mx-auto\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(subpage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/account/container.templ`, Line: 16, Col: 13}
|
||||
}
|
||||
_, 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, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
switch subpage {
|
||||
case "General":
|
||||
templ_7745c5c3_Err = AccountGeneral().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "Security":
|
||||
templ_7745c5c3_Err = AccountSecurity().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,52 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package account
|
||||
|
||||
//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"
|
||||
|
||||
func AccountGeneral() 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = ChangeUsername("", "").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = ChangeBio("", "").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,48 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package account
|
||||
|
||||
//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"
|
||||
|
||||
func AccountSecurity() 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = ChangePassword("").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,131 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package account
|
||||
|
||||
//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 "fmt"
|
||||
|
||||
type MenuItem struct {
|
||||
name string
|
||||
href string
|
||||
}
|
||||
|
||||
func getMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{
|
||||
name: "General",
|
||||
href: "general",
|
||||
},
|
||||
{
|
||||
name: "Security",
|
||||
href: "security",
|
||||
},
|
||||
{
|
||||
name: "Preferences",
|
||||
href: "preferences",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func SelectMenu(activePage 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)
|
||||
|
||||
menuItems := getMenuItems()
|
||||
page := fmt.Sprintf("{page:'%s'}", activePage)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form hx-post=\"/account-select-page\" hx-target=\"#account-container\" hx-swap=\"outerHTML\" class=\"relative\"><div class=\"bg-surface0 border-e border-overlay0 ease-in-out\n absolute top-0 left-0 z-1\n rounded-l-xl h-full overflow-hidden transition-all duration-300\" x-bind:style=\"(open || big) ? 'width: 200px;' : 'width: 40px;'\"><div x-show=\"!big\"><button type=\"button\" @click=\"open = !open\" class=\"block rounded-lg p-2.5 md:hidden transition\n bg-surface0 text-subtext0 hover:text-overlay2/75\"><span class=\"sr-only\">Toggle menu</span> <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"size-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4 6h16M4 12h16M4 18h16\"></path></svg></button></div><div class=\"px-4 py-6\" x-show=\"(open || big)\"><ul class=\"mt-6 space-y-1\" x-data=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(page)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/account/selectmenu.templ`, Line: 69, Col: 44}
|
||||
}
|
||||
_, 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, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range menuItems {
|
||||
|
||||
activebind := fmt.Sprintf("page === '%s' && 'bg-mantle'", item.name)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<li><button type=\"submit\" name=\"subpage\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/account/selectmenu.templ`, Line: 78, Col: 25}
|
||||
}
|
||||
_, 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, 4, "\" class=\"block rounded-lg px-4 py-2 text-md\n hover:bg-mantle hover:cursor-pointer\" :class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(activebind)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/account/selectmenu.templ`, Line: 81, Col: 27}
|
||||
}
|
||||
_, 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, 5, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/account/selectmenu.templ`, Line: 83, Col: 19}
|
||||
}
|
||||
_, 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, 6, "</button></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</ul></div></div></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,92 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package footer
|
||||
|
||||
//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 FooterItem struct {
|
||||
name string
|
||||
href string
|
||||
}
|
||||
|
||||
// Specify the links to show in the footer
|
||||
func getFooterItems() []FooterItem {
|
||||
return []FooterItem{
|
||||
{
|
||||
name: "About",
|
||||
href: "/about",
|
||||
},
|
||||
{
|
||||
name: "Github",
|
||||
href: "https://github.com/haelnorr/projectreshoot",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the template fragment for the Footer
|
||||
func Footer() 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<footer class=\"bg-mantle mt-10\"><div class=\"relative mx-auto max-w-screen-xl px-4 py-8 sm:px-6 lg:px-8\"><div class=\"absolute end-4 top-4 sm:end-6 lg:end-8\"><a class=\"inline-block rounded-full bg-teal p-2 text-crust\n shadow-sm transition hover:bg-teal/75\" href=\"#main-content\"><span class=\"sr-only\">Back to top</span> <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"size-5\" viewBox=\"0 0 20 20\" fill=\"currentColor\"><path fill-rule=\"evenodd\" d=\"M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 \n 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 \n 4a1 1 0 010 1.414z\" clip-rule=\"evenodd\"></path></svg></a></div><div class=\"lg:flex lg:items-end lg:justify-between\"><div><div class=\"flex justify-center text-text lg:justify-start\"><span class=\"text-2xl\">Project Reshoot</span></div><p class=\"mx-auto max-w-md text-center leading-relaxed\n text-subtext0\">A better way to discover and rate films</p></div><ul class=\"mt-12 flex flex-wrap justify-center gap-6 md:gap-8\n lg:mt-0 lg:justify-end lg:gap-12\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range getFooterItems() {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<li><a class=\"transition hover:text-subtext1\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL(item.href)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/footer/footer.templ`, Line: 71, Col: 19}
|
||||
}
|
||||
_, 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, 4, "</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</ul></div><div class=\"lg:flex lg:items-end lg:justify-between\"><div><p class=\"mt-4 text-center text-sm text-overlay0\">by Haelnorr | <a href=\"#\">Film data</a> from <a href=\"https://www.themoviedb.org/\" class=\"underline hover:text-subtext0 transition\">TMDB</a></p></div><div><div class=\"mt-2 text-center\"><label for=\"theme-select\" class=\"hidden lg:inline\">Theme</label> <select name=\"ThemeSelect\" id=\"theme-select\" class=\"mt-1.5 inline rounded-lg bg-surface0 p-2 w-fit\" x-model=\"theme\"><template x-for=\"themeopt in [\n 'dark',\n 'light',\n 'system',\n ]\"><option x-text=\"displayThemeName(themeopt)\" :value=\"themeopt\" :selected=\"theme === themeopt\"></option></template></select><script>\n const displayThemeName = (value) => {\n if (value === \"dark\") return \"Dark (Mocha)\";\n if (value === \"light\") return \"Light (Latte)\";\n if (value === \"system\") return \"System\";\n }\n </script></div></div></div></div></footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,55 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package form
|
||||
|
||||
//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"
|
||||
|
||||
func ConfirmPassword(err 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form hx-post=\"/reauthenticate\" x-data=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSFuncCall(
|
||||
"confirmPassData", err,
|
||||
).CallInline)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/form/confirmpass.templ`, Line: 8, Col: 28}
|
||||
}
|
||||
_, 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, "\" x-on:htmx:xhr:loadstart=\"submitted=true;buttontext='Loading...'\"><script>\n function confirmPassData(err) {\n return {\n submitted: false,\n buttontext: 'Confirm', \n errMsg: err,\n reset() {\n this.err = \"\";\n },\n };\n }\n </script><div class=\"grid gap-y-4\"><div class=\"mt-5\"><div class=\"relative\"><input type=\"password\" id=\"password\" name=\"password\" class=\"py-3 px-4 block w-full rounded-lg text-sm\n focus:border-blue focus:ring-blue bg-base\n disabled:opacity-50 disabled:pointer-events-none\" placeholder=\"Confirm password\" required aria-describedby=\"password-error\" @input=\"reset()\"><div class=\"absolute inset-y-0 end-0 \n pointer-events-none pe-3 pt-3\" x-show=\"errMsg\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0\n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1\n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div></div><p class=\"text-center text-xs text-red mt-2\" id=\"password-error\" x-show=\"errMsg\" x-cloak x-text=\"errMsg\"></p></div><button x-bind:disabled=\"submitted\" x-text=\"buttontext\" type=\"submit\" class=\"w-full py-3 px-4 inline-flex justify-center items-center \n gap-x-2 rounded-lg border border-transparent transition\n bg-blue hover:bg-blue/75 text-mantle hover:cursor-pointer\n disabled:bg-blue/60 disabled:cursor-default\"></button> <button type=\"button\" class=\"w-full py-3 px-4 inline-flex justify-center items-center \n gap-x-2 rounded-lg border border-transparent transition\n bg-surface2 hover:bg-surface1 hover:cursor-pointer\n disabled:cursor-default\" @click=\"showConfirmPasswordModal=false\">Cancel</button></div></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,60 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package form
|
||||
|
||||
//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"
|
||||
|
||||
// Login Form. If loginError is not an empty string, it will display the
|
||||
// contents of loginError to the user.
|
||||
// If loginError is "Username or password incorrect" it will also show
|
||||
// error icons on the username and password field
|
||||
func LoginForm(loginError 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)
|
||||
credErr := "Username or password incorrect"
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form hx-post=\"/login\" x-data=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSFuncCall(
|
||||
"loginFormData", loginError, credErr,
|
||||
).CallInline)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/form/loginform.templ`, Line: 13, Col: 28}
|
||||
}
|
||||
_, 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, "\" x-on:htmx:xhr:loadstart=\"submitted=true;buttontext='Loading...'\"><script>\n function loginFormData(err, credError) {\n return {\n submitted: false,\n buttontext: 'Login',\n errorMessage: err, \n credentialError: err === credError ? true : false,\n resetErr() {\n this.errorMessage = \"\";\n this.credentialError = false;\n },\n };\n }\n </script><div class=\"grid gap-y-4\"><!-- Form Group --><div><label for=\"username\" class=\"block text-sm mb-2\">Username</label><div class=\"relative\"><input type=\"text\" idnutanix=\"username\" name=\"username\" class=\"py-3 px-4 block w-full rounded-lg text-sm\n focus:border-blue focus:ring-blue bg-base\n disabled:opacity-50 \n disabled:pointer-events-none\" required aria-describedby=\"username-error\" @input=\"resetErr()\"><div class=\"absolute inset-y-0 end-0 \n pointer-events-none pe-3 pt-3\" x-show=\"credentialError\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 \n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 \n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div></div></div><div><div class=\"flex justify-between items-center\"><label for=\"password\" class=\"block text-sm mb-2\">Password</label> <a class=\"inline-flex items-center gap-x-1 text-sm \n text-blue decoration-2 hover:underline \n focus:outline-none focus:underline font-medium\" href=\"/recover-account\" tabindex=\"-1\">Forgot password?</a></div><div class=\"relative\"><input type=\"password\" id=\"password\" name=\"password\" class=\"py-3 px-4 block w-full rounded-lg text-sm\n focus:border-blue focus:ring-blue bg-base\n disabled:opacity-50 disabled:pointer-events-none\" required aria-describedby=\"password-error\" @input=\"resetErr()\"><div class=\"absolute inset-y-0 end-0 \n pointer-events-none pe-3 pt-3\" x-show=\"credentialError\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0\n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1\n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div></div><p class=\"text-center text-xs text-red mt-2\" id=\"password-error\" x-show=\"errorMessage\" x-cloak x-text=\"errorMessage\"></p></div><div class=\"flex items-center\"><div class=\"flex\"><input id=\"remember-me\" name=\"remember-me\" type=\"checkbox\" class=\"shrink-0 mt-0.5 border-gray-200 rounded\n text-blue focus:ring-blue-500\"></div><div class=\"ms-3\"><label for=\"remember-me\" class=\"text-sm\">Remember me</label></div></div><button x-bind:disabled=\"submitted\" x-text=\"buttontext\" type=\"submit\" class=\"w-full py-3 px-4 inline-flex justify-center items-center \n gap-x-2 rounded-lg border border-transparent transition\n bg-green hover:bg-green/75 text-mantle hover:cursor-pointer\n disabled:bg-green/60 disabled:cursor-default\"></button></div></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,63 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package form
|
||||
|
||||
//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"
|
||||
|
||||
// Login Form. If loginError is not an empty string, it will display the
|
||||
// contents of loginError to the user.
|
||||
func RegisterForm(registerError 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)
|
||||
|
||||
usernameErr := "Username is taken"
|
||||
passErrs := []string{
|
||||
"Password exceeds maximum length of 72 bytes",
|
||||
"Passwords do not match",
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form hx-post=\"/register\" x-data=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSFuncCall(
|
||||
"registerFormData", registerError, usernameErr, passErrs,
|
||||
).CallInline)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/form/registerform.templ`, Line: 17, Col: 28}
|
||||
}
|
||||
_, 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, "\" x-on:htmx:xhr:loadstart=\"submitted=true;buttontext='Loading...'\"><script>\n function registerFormData(err, usernameErr, passErrs) {\n return {\n submitted: false,\n buttontext: 'Register',\n errorMessage: err, \n errUsername: err === usernameErr ? true : false, \n errPasswords: passErrs.includes(err) ? true : false,\n resetErr() {\n this.errorMessage = \"\";\n this.errUsername = false;\n this.errPasswords = false;\n },\n };\n }\n </script><div class=\"grid gap-y-4\"><div><label for=\"username\" class=\"block text-sm mb-2\">Username</label><div class=\"relative\"><input type=\"text\" id=\"username\" name=\"username\" class=\"py-3 px-4 block w-full rounded-lg text-sm\n focus:border-blue focus:ring-blue bg-base\n disabled:opacity-50 \n disabled:pointer-events-none\" required aria-describedby=\"username-error\" @input=\"resetErr()\"><div class=\"absolute inset-y-0 end-0 \n pointer-events-none pe-3 pt-3\" x-show=\"errUsername\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0 \n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1 \n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div><p class=\"text-center text-xs text-red mt-2\" id=\"username-error\" x-show=\"errUsername\" x-cloak x-text=\"if (errUsername) return errorMessage;\"></p></div></div><div><div class=\"flex justify-between items-center\"><label for=\"password\" class=\"block text-sm mb-2\">Password</label></div><div class=\"relative\"><input type=\"password\" id=\"password\" name=\"password\" class=\"py-3 px-4 block w-full rounded-lg text-sm\n focus:border-blue focus:ring-blue bg-base\n disabled:opacity-50 disabled:pointer-events-none\" required aria-describedby=\"password-error\" @input=\"resetErr()\"><div class=\"absolute inset-y-0 end-0 \n pointer-events-none pe-3 pt-3\" x-show=\"errPasswords\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0\n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1\n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div></div></div><div><div class=\"flex justify-between items-center\"><label for=\"confirm-password\" class=\"block text-sm mb-2\">Confirm Password</label></div><div class=\"relative\"><input type=\"password\" id=\"confirm-password\" name=\"confirm-password\" class=\"py-3 px-4 block w-full rounded-lg text-sm\n focus:border-blue focus:ring-blue bg-base\n disabled:opacity-50 disabled:pointer-events-none\" required aria-describedby=\"confirm-password-error\" @input=\"resetErr()\"><div class=\"absolute inset-y-0 end-0 \n pointer-events-none pe-3 pt-3\" x-show=\"errPasswords\" x-cloak><svg class=\"size-5 text-red\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path d=\"M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM8 \n 4a.905.905 0 0 0-.9.995l.35 3.507a.552.552 0 0\n 0 1.1 0l.35-3.507A.905.905 0 0 0 8 4zm.002 6a1\n 1 0 1 0 0 2 1 1 0 0 0 0-2z\"></path></svg></div></div><p class=\"text-center text-xs text-red mt-2\" id=\"password-error\" x-show=\"errPasswords\" x-cloak x-text=\"if (errPasswords) return errorMessage;\"></p></div><div class=\"flex items-center\"><div class=\"flex\"><input id=\"remember-me\" name=\"remember-me\" type=\"checkbox\" class=\"shrink-0 mt-0.5 border-gray-200 rounded\n text-blue focus:ring-blue-500\"></div><div class=\"ms-3\"><label for=\"remember-me\" class=\"text-sm\">Remember me</label></div></div><button x-bind:disabled=\"submitted\" x-text=\"buttontext\" type=\"submit\" class=\"w-full py-3 px-4 inline-flex justify-center items-center \n gap-x-2 rounded-lg border border-transparent transition\n bg-green hover:bg-green/75 text-mantle hover:cursor-pointer\n disabled:bg-green/60 disabled:cursor-default\"></button></div></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,77 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package nav
|
||||
|
||||
//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 NavItem struct {
|
||||
name string // Label to display
|
||||
href string // Link reference
|
||||
}
|
||||
|
||||
// Return the list of navbar links
|
||||
func getNavItems() []NavItem {
|
||||
return []NavItem{
|
||||
{
|
||||
name: "Movies",
|
||||
href: "/movies",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the navbar template fragment
|
||||
func Navbar() 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)
|
||||
navItems := getNavItems()
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"{ open: false }\"><header class=\"bg-crust\"><div class=\"mx-auto flex h-16 max-w-screen-xl items-center gap-8\n px-4 sm:px-6 lg:px-8\"><a class=\"block\" href=\"/\"><!-- logo here --><span class=\"text-3xl font-bold transition hover:text-green\"><span class=\"hidden sm:inline\">Project</span> Reshoot</span></a><div class=\"flex flex-1 items-center justify-end sm:justify-between\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = navLeft(navItems).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = navRight().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></div></header>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = sideNav(navItems).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,73 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package nav
|
||||
|
||||
//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"
|
||||
|
||||
// Returns the left portion of the navbar
|
||||
func navLeft(navItems []NavItem) 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<nav aria-label=\"Global\" class=\"hidden sm:block\"><ul class=\"flex items-center gap-6 text-xl\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range navItems {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<li><a class=\"text-subtext1 hover:text-green transition\" href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL(item.href)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/nav/navbarleft.templ`, Line: 13, Col: 17}
|
||||
}
|
||||
_, 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, 4, "</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</ul></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,124 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package nav
|
||||
|
||||
//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 "projectreshoot/pkg/contexts"
|
||||
|
||||
type ProfileItem struct {
|
||||
name string // Label to display
|
||||
href string // Link reference
|
||||
}
|
||||
|
||||
// Return the list of profile links
|
||||
func getProfileItems() []ProfileItem {
|
||||
return []ProfileItem{
|
||||
{
|
||||
name: "Profile",
|
||||
href: "/profile",
|
||||
},
|
||||
{
|
||||
name: "Account",
|
||||
href: "/account",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the right portion of the navbar
|
||||
func navRight() 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)
|
||||
user := contexts.GetUser(ctx)
|
||||
items := getProfileItems()
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex items-center gap-2\"><div class=\"sm:flex sm:gap-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user != nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div x-data=\"{ isActive: false }\" class=\"relative\"><div class=\"inline-flex items-center overflow-hidden\n rounded-lg bg-sapphire hover:bg-sapphire/75 transition\"><button x-on:click=\"isActive = !isActive\" class=\"h-full py-2 px-4 text-mantle hover:cursor-pointer\"><span class=\"sr-only\">Profile</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/nav/navbarright.templ`, Line: 41, 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, 3, "</button></div><div class=\"absolute end-0 z-10 mt-2 w-36 divide-y \n divide-surface2 rounded-lg border border-surface1 \n bg-surface0 shadow-lg\" role=\"menu\" x-cloak x-transition x-show=\"isActive\" x-on:click.away=\"isActive = false\" x-on:keydown.escape.window=\"isActive = false\"><div class=\"p-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range items {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL(item.href)
|
||||
_, 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, 5, "\" class=\"block rounded-lg px-4 py-2 text-md \n hover:bg-crust\" role=\"menuitem\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/nav/navbarright.templ`, Line: 63, Col: 20}
|
||||
}
|
||||
_, 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, 6, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div class=\"p-2\"><form hx-post=\"/logout\"><button type=\"submit\" class=\"flex w-full items-center gap-2\n rounded-lg px-4 py-2 text-md text-red \n hover:bg-red/25 hover:cursor-pointer\" role=\"menuitem\" @click=\"isActive=false\">Logout</button></form></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a class=\"hidden rounded-lg px-4 py-2 sm:block \n bg-green hover:bg-green/75 text-mantle transition\" href=\"/login\">Login</a> <a class=\"hidden rounded-lg px-4 py-2 sm:block\n bg-blue text-mantle hover:bg-blue/75 transition\" href=\"/register\">Register</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div><button @click=\"open = !open\" class=\"block rounded-lg p-2.5 sm:hidden transition\n bg-surface0 text-subtext0 hover:text-overlay2/75\"><span class=\"sr-only\">Toggle menu</span> <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"size-5\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4 6h16M4 12h16M4 18h16\"></path></svg></button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,86 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package nav
|
||||
|
||||
//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 "projectreshoot/pkg/contexts"
|
||||
|
||||
// Returns the mobile version of the navbar thats only visible when activated
|
||||
func sideNav(navItems []NavItem) 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)
|
||||
user := contexts.GetUser(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-show=\"open\" x-transition class=\"absolute w-full bg-mantle sm:hidden z-10\"><div class=\"px-4 py-6\"><ul class=\"space-y-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range navItems {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<li><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL(item.href)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"block rounded-lg px-4 py-2 text-lg\n bg-surface0 text-text transition hover:bg-surface2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/nav/sidenav.templ`, Line: 22, Col: 18}
|
||||
}
|
||||
_, 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, 4, "</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</ul></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user == nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"px-4 pb-6\"><ul class=\"space-y-1\"><li class=\"flex justify-center items-center gap-2\"><a class=\"w-26 px-4 py-2 rounded-lg\n bg-green text-mantle transition hover:bg-green/75\n text-center\" href=\"/login\">Login</a> <a class=\"w-26 px-4 py-2 rounded-lg\n bg-blue text-mantle transition hover:bg-blue/75\n text-center\" href=\"/register\">Register</a></li></ul></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,51 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
|
||||
package popup
|
||||
|
||||
//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 "projectreshoot/internal/view/component/form"
|
||||
|
||||
func ConfirmPasswordModal() 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"z-50 absolute bg-overlay0/55 top-0 left-0 right-0 bottom-0\" x-show=\"showConfirmPasswordModal\" x-cloak><div class=\"p-5 mt-25 w-fit max-w-100 text-center rounded-lg bg-mantle mx-auto\"><div class=\"text-xl\">To complete this action you need to confirm your password</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = form.ConfirmPassword("").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,40 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package popup
|
||||
|
||||
//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"
|
||||
|
||||
func Error500Popup() 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-cloak x-show=\"showError500\" class=\"absolute w-82 left-0 right-0 mt-20 mr-5 ml-auto\" x-transition:enter=\"transform translate-x-[100%] opacity-0 duration-200\" x-transition:enter-start=\"opacity-0 translate-x-[100%]\" x-transition:enter-end=\"opacity-100 translate-x-0\" x-transition:leave=\"opacity-0 duration-200\" x-transition:leave-start=\"opacity-100 translate-x-0\" x-transition:leave-end=\"opacity-0 translate-x-[100%]\"><div role=\"alert\" class=\"rounded-sm bg-dark-red p-4\"><div class=\"flex justify-between\"><div class=\"flex items-center gap-2 text-red w-fit\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" class=\"size-5\"><path fill-rule=\"evenodd\" d=\"M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 \n 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 \n 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 \n 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 \n 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z\" clip-rule=\"evenodd\"></path></svg> <strong class=\"block font-medium\">Something went wrong </strong></div><div class=\"flex\"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6 text-subtext0 hover:cursor-pointer\" @click=\"showError500=false\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"></path></svg></div></div><p class=\"mt-2 text-sm text-red\">An error occured on the server. Please try again later, or contact an administrator</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,40 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package popup
|
||||
|
||||
//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"
|
||||
|
||||
func Error503Popup() 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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-cloak x-show=\"showError503\" class=\"absolute w-82 left-0 right-0 mt-20 mr-5 ml-auto\" x-transition:enter=\"transform translate-x-[100%] opacity-0 duration-200\" x-transition:enter-start=\"opacity-0 translate-x-[100%]\" x-transition:enter-end=\"opacity-100 translate-x-0\" x-transition:leave=\"opacity-0 duration-200\" x-transition:leave-start=\"opacity-100 translate-x-0\" x-transition:leave-end=\"opacity-0 translate-x-[100%]\"><div role=\"alert\" class=\"rounded-sm bg-dark-red p-4\"><div class=\"flex justify-between\"><div class=\"flex items-center gap-2 text-red w-fit\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" class=\"size-5\"><path fill-rule=\"evenodd\" d=\"M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 \n 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 \n 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 \n 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 \n 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z\" clip-rule=\"evenodd\"></path></svg> <strong class=\"block font-medium\">Service Unavailable</strong></div><div class=\"flex\"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"size-6 text-subtext0 hover:cursor-pointer\" @click=\"showError503=false\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"></path></svg></div></div><p class=\"mt-2 text-sm text-red\">The service is currently available. It could be down for maintenance. Please try again later.</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,7 +1,7 @@
|
||||
package search
|
||||
|
||||
import "projectreshoot/pkg/tmdb"
|
||||
import "fmt"
|
||||
import "git.haelnorr.com/h/golib/tmdb"
|
||||
|
||||
templ MovieResults(movies *tmdb.ResultMovies, image *tmdb.Image) {
|
||||
for _, movie := range movies.Results {
|
||||
@@ -18,23 +18,23 @@ templ MovieResults(movies *tmdb.ResultMovies, image *tmdb.Image) {
|
||||
onerror="this.onerror=null; setFallbackColor(this);"
|
||||
/>
|
||||
<script>
|
||||
function setFallbackColor(img) {
|
||||
const baseColor = getComputedStyle(document.documentElement).
|
||||
getPropertyValue('--base').trim();
|
||||
img.src = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='144'%3E%3Crect width='100%' height='100%' fill='${baseColor}'/%3E%3C/svg%3E`;
|
||||
}
|
||||
</script>
|
||||
function setFallbackColor(img) {
|
||||
const baseColor = getComputedStyle(document.documentElement).
|
||||
getPropertyValue('--base').trim();
|
||||
img.src = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='144'%3E%3Crect width='100%' height='100%' fill='${baseColor}'/%3E%3C/svg%3E`;
|
||||
}
|
||||
</script>
|
||||
<div>
|
||||
<a
|
||||
href={ templ.SafeURL(fmt.Sprintf("/movie/%v", movie.ID)) }
|
||||
class="text-xl font-semibold transition hover:text-green"
|
||||
>{ movie.Title } { movie.ReleaseYear() }</a>
|
||||
<p class="text-subtext0">
|
||||
Released:
|
||||
Released:
|
||||
<span class="font-medium">{ movie.ReleaseDate }</span>
|
||||
</p>
|
||||
<p class="text-subtext0">
|
||||
Original Title:
|
||||
Original Title:
|
||||
<span class="font-medium">{ movie.OriginalTitle }</span>
|
||||
</p>
|
||||
<p class="text-subtext0">{ movie.Overview }</p>
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package search
|
||||
|
||||
//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 "projectreshoot/pkg/tmdb"
|
||||
import "fmt"
|
||||
|
||||
func MovieResults(movies *tmdb.ResultMovies, image *tmdb.Image) 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)
|
||||
for _, movie := range movies.Results {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"bg-surface0 p-4 rounded-lg shadow-lg flex \n items-start space-x-4\"><img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(movie.GetPoster(image, "w92"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/search/movies_results.templ`, Line: 13, Col: 39}
|
||||
}
|
||||
_, 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, "\" alt=\"Movie Poster\" class=\"rounded-lg object-cover\" width=\"96\" height=\"144\" onerror=\"this.onerror=null; setFallbackColor(this);\"><script>\n function setFallbackColor(img) {\n const baseColor = getComputedStyle(document.documentElement).\n getPropertyValue('--base').trim();\n img.src = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='144'%3E%3Crect width='100%' height='100%' fill='${baseColor}'/%3E%3C/svg%3E`;\n }\n </script><div><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/movie/%v", movie.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, "\" class=\"text-xl font-semibold transition hover:text-green\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(movie.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/search/movies_results.templ`, Line: 31, Col: 18}
|
||||
}
|
||||
_, 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(movie.ReleaseYear())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/search/movies_results.templ`, Line: 31, Col: 42}
|
||||
}
|
||||
_, 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, "</a><p class=\"text-subtext0\">Released: <span class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(movie.ReleaseDate)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/search/movies_results.templ`, Line: 34, Col: 50}
|
||||
}
|
||||
_, 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, "</span></p><p class=\"text-subtext0\">Original Title: <span class=\"font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(movie.OriginalTitle)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/search/movies_results.templ`, Line: 38, Col: 52}
|
||||
}
|
||||
_, 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, "</span></p><p class=\"text-subtext0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(movie.Overview)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/component/search/movies_results.templ`, Line: 40, Col: 45}
|
||||
}
|
||||
_, 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, "</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,99 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package layout
|
||||
|
||||
//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 "projectreshoot/internal/view/component/nav"
|
||||
import "projectreshoot/internal/view/component/footer"
|
||||
import "projectreshoot/internal/view/component/popup"
|
||||
|
||||
// Global page layout. Includes HTML document settings, header tags
|
||||
// navbar and footer
|
||||
func Global(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_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" x-data=\"{\n theme: localStorage.getItem('theme')\n || 'system'}\" x-init=\"$watch('theme', (val) => localStorage.setItem('theme', val))\" x-bind:class=\"{'dark': theme === 'dark' || (theme === 'system' &&\n window.matchMedia('(prefers-color-scheme: dark)').matches)}\"><head><script>\n (function () {\n let theme = localStorage.getItem(\"theme\") || \"system\";\n if (theme === \"system\") {\n theme = window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n }\n if (theme === \"dark\") {\n document.documentElement.classList.add(\"dark\");\n } else {\n document.documentElement.classList.remove(\"dark\");\n }\n })();\n </script><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(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/layout/global.templ`, Line: 36, Col: 17}
|
||||
}
|
||||
_, 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=\"/static/favicon.ico\"><link href=\"/static/css/output.css\" rel=\"stylesheet\"><script src=\"https://unpkg.com/htmx.org@2.0.4\" integrity=\"sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+\" crossorigin=\"anonymous\"></script><script defer src=\"https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js\"></script><script src=\"https://unpkg.com/alpinejs\" defer></script><script>\n // uncomment this line to enable logging of htmx events\n // htmx.logAll();\n </script><script>\n const bodyData = {\n showError500: false,\n showError503: false,\n showConfirmPasswordModal: false,\n handleHtmxBeforeOnLoad(event) {\n const requestPath = event.detail.pathInfo.requestPath;\n if (requestPath === \"/reauthenticate\") {\n // handle password incorrect on refresh attempt\n if (event.detail.xhr.status === 445) {\n event.detail.shouldSwap = true;\n event.detail.isError = false;\n } else if (event.detail.xhr.status === 200) {\n this.showConfirmPasswordModal = false;\n }\n }\n },\n // handle errors from the server on HTMX requests\n handleHtmxError(event) {\n const errorCode = event.detail.errorInfo.error;\n \n // internal server error \n if (errorCode.includes('Code 500')) {\n this.showError500 = true;\n setTimeout(() => this.showError500 = false, 6000);\n }\n // service not available error\n if (errorCode.includes('Code 503')) {\n this.showError503 = true;\n setTimeout(() => this.showError503 = false, 6000);\n }\n \n // user is authorized but needs to refresh their login\n if (errorCode.includes('Code 444')) {\n this.showConfirmPasswordModal = true;\n }\n },\n };\n </script></head><body class=\"bg-base text-text ubuntu-mono-regular overflow-x-hidden\" x-data=\"bodyData\" x-on:htmx:error=\"handleHtmxError($event)\" x-on:htmx:before-on-load=\"handleHtmxBeforeOnLoad($event)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = popup.Error500Popup().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = popup.Error503Popup().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = popup.ConfirmPasswordModal().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div id=\"main-content\" class=\"flex flex-col h-screen justify-between\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = nav.Navbar().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div id=\"page-content\" class=\"mb-auto md:px-5 md:pt-5\">")
|
||||
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, 5, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = footer.Footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,61 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/internal/view/layout"
|
||||
|
||||
// Returns the about page content
|
||||
func About() 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=\"text-center max-w-150 m-auto\"><div class=\"text-4xl mt-8\">About</div><div class=\"text-xl font-bold mt-4\">What is Project Reshoot?</div><div class=\"text-lg mt-2\">Project Reshoot is a movie review site that aims to provide a better experience for the users. Instead of a single number that shows the average, or a spread of star ratings, Project Reshoot asks you to rate movies with a vibe. These ratings are shown as an easy to see pie chart showing how everyone felt.</div><div class=\"text-lg mt-2\">The other major feature is the ability for you to customize what details you see about movies, hiding details you don't want to see until after you've watched it. This gives you peace of mind when searching for new movies to watch.</div><div class=\"text-xl font-bold mt-4\">Why the name?</div><div class=\"text-lg mt-2\">The name came partially from the premise of wanting to deliver a new take on movie reviews, and partially from it being rewritten from scratch in a new technology stack (Goodbye NextJS, Hello GOTH).</div><div class=\"text-xl font-bold mt-4\">Who's behind it?</div><div class=\"text-lg mt-2\">Currently Project Reshoot is being built by a team of 1. It is somewhat of a passion project and a way to practice my development skills. For the time being, it will likely stay that way, but if you want to contribute, you can check out the <a href=\"https://github.com/haelnorr/moviedb\">Github repo here</a>.</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global("About").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,61 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/internal/view/layout"
|
||||
import "projectreshoot/internal/view/component/account"
|
||||
|
||||
func Account(subpage 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)
|
||||
templ_7745c5c3_Err = account.AccountContainer(subpage).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global("Account - "+subpage).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,103 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/internal/view/layout"
|
||||
import "strconv"
|
||||
|
||||
// Page template for Error pages. Error code should be a HTTP status code as
|
||||
// a string, and err should be the corresponding response title.
|
||||
// Message is a custom error message displayed below the code and error.
|
||||
func Error(code int, err string, message 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)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"grid mt-24 left-0 right-0 top-0 bottom-0 \n place-content-center bg-base px-4\"><div class=\"text-center\"><h1 class=\"text-9xl text-text\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(code))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/error.templ`, Line: 18, Col: 25}
|
||||
}
|
||||
_, 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, "</h1><p class=\"text-2xl font-bold tracking-tight text-subtext1\n sm:text-4xl\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(err)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/error.templ`, Line: 22, Col: 10}
|
||||
}
|
||||
_, 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, 3, "</p><p class=\"mt-4 text-subtext0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/error.templ`, Line: 25, Col: 14}
|
||||
}
|
||||
_, 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, 4, "</p><a href=\"/\" class=\"mt-6 inline-block rounded-lg bg-mauve px-5 py-3 \n text-sm text-crust transition hover:bg-mauve/75\">Go to homepage</a></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global(err).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,61 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/internal/view/layout"
|
||||
|
||||
// Page content for the index page
|
||||
func Index() 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=\"text-center mt-24\"><div class=\"text-4xl lg:text-6xl\">Project Reshoot</div><div>A better way to discover and rate films</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global("Project Reshoot").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,70 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/internal/view/layout"
|
||||
import "projectreshoot/internal/view/component/form"
|
||||
|
||||
// Returns the login page
|
||||
func Login() 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=\"max-w-100 mx-auto px-2\"><div class=\"mt-7 bg-mantle border border-surface1 rounded-xl\"><div class=\"p-4 sm:p-7\"><div class=\"text-center\"><h1 class=\"block text-2xl font-bold\">Login</h1><p class=\"mt-2 text-sm text-subtext0\">Don't have an account yet? <a class=\"text-blue decoration-2 hover:underline \n focus:outline-none focus:underline\" href=\"/register\">Sign up here</a></p></div><div class=\"mt-5\"><div class=\"py-3 flex items-center text-xs text-subtext0 \n uppercase before:flex-1 before:border-t \n before:border-overlay1 before:me-6 after:flex-1 \n after:border-t after:border-overlay1 after:ms-6\">Or</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = form.LoginForm("").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global("Login").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,15 +1,12 @@
|
||||
package page
|
||||
|
||||
import "projectreshoot/pkg/tmdb"
|
||||
import "git.haelnorr.com/h/golib/tmdb"
|
||||
import "projectreshoot/internal/view/layout"
|
||||
|
||||
templ Movie(movie *tmdb.Movie, credits *tmdb.Credits, image *tmdb.Image) {
|
||||
@layout.Global(movie.Title) {
|
||||
<div class="md:bg-surface0 md:p-2 md:rounded-lg transition-all">
|
||||
<div
|
||||
id="billedcrew"
|
||||
class="hidden"
|
||||
>
|
||||
<div id="billedcrew" class="hidden">
|
||||
for _, billedcrew := range credits.BilledCrew() {
|
||||
<span class="flex flex-col text-left w-[130px] md:w-[180px]">
|
||||
<span class="font-bold">{ billedcrew.Name }</span>
|
||||
@@ -20,7 +17,7 @@ templ Movie(movie *tmdb.Movie, credits *tmdb.Credits, image *tmdb.Image) {
|
||||
<div class="flex items-start">
|
||||
<div class="w-[154px] md:w-[300px] flex-col">
|
||||
<img
|
||||
class="object-cover aspect-[2/3] w-[154px] md:w-[300px]
|
||||
class="object-cover aspect-2/3 w-[154px] md:w-[300px]
|
||||
transition-all md:rounded-md shadow-black shadow-2xl"
|
||||
src={ movie.GetPoster(image, "w300") }
|
||||
alt="Poster"
|
||||
@@ -31,27 +28,27 @@ templ Movie(movie *tmdb.Movie, credits *tmdb.Credits, image *tmdb.Image) {
|
||||
mt-5 flex-wrap justify-around flex-col px-5 md:hidden"
|
||||
></div>
|
||||
<script>
|
||||
function moveBilledCrew() {
|
||||
const billedCrewMd = document.getElementById('billedcrew-md');
|
||||
const billedCrewSm = document.getElementById('billedcrew-sm');
|
||||
const billedCrew = document.getElementById('billedcrew');
|
||||
function moveBilledCrew() {
|
||||
const billedCrewMd = document.getElementById('billedcrew-md');
|
||||
const billedCrewSm = document.getElementById('billedcrew-sm');
|
||||
const billedCrew = document.getElementById('billedcrew');
|
||||
|
||||
if (window.innerWidth < 768) {
|
||||
billedCrewSm.innerHTML = billedCrew.innerHTML;
|
||||
billedCrewMd.innerHTML = "";
|
||||
} else {
|
||||
billedCrewMd.innerHTML = billedCrew.innerHTML;
|
||||
billedCrewSm.innerHTML = "";
|
||||
}
|
||||
}
|
||||
if (window.innerWidth < 768) {
|
||||
billedCrewSm.innerHTML = billedCrew.innerHTML;
|
||||
billedCrewMd.innerHTML = "";
|
||||
} else {
|
||||
billedCrewMd.innerHTML = billedCrew.innerHTML;
|
||||
billedCrewSm.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', moveBilledCrew);
|
||||
window.addEventListener('load', moveBilledCrew);
|
||||
|
||||
const resizeObs = new ResizeObserver(() => {
|
||||
moveBilledCrew();
|
||||
});
|
||||
resizeObs.observe(document.body);
|
||||
</script>
|
||||
const resizeObs = new ResizeObserver(() => {
|
||||
moveBilledCrew();
|
||||
});
|
||||
resizeObs.observe(document.body);
|
||||
</script>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 text-center px-4">
|
||||
<span class="text-xl md:text-3xl font-semibold">
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/internal/view/layout"
|
||||
|
||||
func Movies() 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=\"max-w-4xl mx-auto md:mt-0 mt-2 px-2 md:px-0\"><form hx-post=\"/search-movies\" hx-target=\"#search-movies-results\"><div class=\"max-w-100 flex items-center space-x-2 mb-2\"><input id=\"search\" name=\"search\" type=\"text\" placeholder=\"Search movies...\" class=\"flex-grow p-2 border rounded-lg \n bg-mantle border-surface2 shadow-sm\n focus:outline-none focus:ring-2 focus:ring-blue\"> <button type=\"submit\" class=\"py-2 px-4 bg-green text-mantle rounded-lg transition\n hover:cursor-pointer hover:bg-green/75\">Search</button></div><div id=\"search-movies-results\" class=\"space-y-4\"></div></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global("Search movies").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,188 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/pkg/tmdb"
|
||||
import "projectreshoot/internal/view/layout"
|
||||
|
||||
func Movie(movie *tmdb.Movie, credits *tmdb.Credits, image *tmdb.Image) 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=\"md:bg-surface0 md:p-2 md:rounded-lg transition-all\"><div id=\"billedcrew\" class=\"hidden\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, billedcrew := range credits.BilledCrew() {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"flex flex-col text-left w-[130px] md:w-[180px]\"><span class=\"font-bold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(billedcrew.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 15, Col: 47}
|
||||
}
|
||||
_, 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, "</span> <span class=\"text-subtext1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(billedcrew.FRoles())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 16, Col: 55}
|
||||
}
|
||||
_, 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, "</span></span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"flex items-start\"><div class=\"w-[154px] md:w-[300px] flex-col\"><img class=\"object-cover aspect-[2/3] w-[154px] md:w-[300px] \n transition-all md:rounded-md shadow-black shadow-2xl\" src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(movie.GetPoster(image, "w300"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 25, Col: 42}
|
||||
}
|
||||
_, 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, 6, "\" alt=\"Poster\"><div id=\"billedcrew-sm\" class=\"text-sm md:text-lg text-subtext1 flex gap-6\n mt-5 flex-wrap justify-around flex-col px-5 md:hidden\"></div><script>\n function moveBilledCrew() {\n const billedCrewMd = document.getElementById('billedcrew-md');\n const billedCrewSm = document.getElementById('billedcrew-sm');\n const billedCrew = document.getElementById('billedcrew');\n\n if (window.innerWidth < 768) {\n billedCrewSm.innerHTML = billedCrew.innerHTML;\n billedCrewMd.innerHTML = \"\";\n } else {\n billedCrewMd.innerHTML = billedCrew.innerHTML;\n billedCrewSm.innerHTML = \"\";\n }\n }\n\n window.addEventListener('load', moveBilledCrew);\n\n const resizeObs = new ResizeObserver(() => {\n moveBilledCrew();\n });\n resizeObs.observe(document.body);\n </script></div><div class=\"flex flex-col flex-1 text-center px-4\"><span class=\"text-xl md:text-3xl font-semibold\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(movie.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 58, Col: 19}
|
||||
}
|
||||
_, 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, 7, "</span> <span class=\"text-sm md:text-lg text-subtext1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(movie.FGenres())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 61, Col: 23}
|
||||
}
|
||||
_, 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, 8, " • ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(movie.FRuntime())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 62, Col: 33}
|
||||
}
|
||||
_, 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, 9, " • ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(movie.ReleaseYear())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 63, Col: 36}
|
||||
}
|
||||
_, 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, 10, "</span><div class=\"flex justify-center gap-2 mt-2\"><div class=\"w-20 h-20 md:w-30 md:h-30 bg-overlay2 \n transition-all rounded-sm\"></div><div class=\"w-20 h-20 md:w-30 md:h-30 bg-overlay2 \n transition-all rounded-sm\"></div></div><div class=\"flex flex-col mt-4\"><span class=\"text-sm md:text-lg text-overlay2 italic\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(movie.Tagline)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 77, Col: 22}
|
||||
}
|
||||
_, 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, 11, "</span><div id=\"billedcrew-md\" class=\"hidden text-sm md:text-lg text-subtext1 md:flex gap-6\n mt-5 flex-wrap justify-around\"></div><span class=\"text-lg mt-5 font-semibold\">Overview</span> <span class=\"text-sm md:text-lg text-subtext1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(movie.Overview)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/movie.templ`, Line: 86, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global(movie.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
|
||||
@@ -1,75 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/internal/view/layout"
|
||||
import "projectreshoot/pkg/contexts"
|
||||
|
||||
func Profile() 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)
|
||||
user := contexts.GetUser(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=\"\">Hello, ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/view/page/profile.templ`, Line: 10, Col: 25}
|
||||
}
|
||||
_, 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>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global("Profile - "+user.Username).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,70 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.833
|
||||
package page
|
||||
|
||||
//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 "projectreshoot/internal/view/layout"
|
||||
import "projectreshoot/internal/view/component/form"
|
||||
|
||||
// Returns the login page
|
||||
func Register() 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=\"max-w-100 mx-auto px-2\"><div class=\"mt-7 bg-mantle border border-surface1 rounded-xl\"><div class=\"p-4 sm:p-7\"><div class=\"text-center\"><h1 class=\"block text-2xl font-bold\">Register</h1><p class=\"mt-2 text-sm text-subtext0\">Already have an account? <a class=\"text-blue decoration-2 hover:underline \n focus:outline-none focus:underline\" href=\"/login\">Login here</a></p></div><div class=\"mt-5\"><div class=\"py-3 flex items-center text-xs text-subtext0 \n uppercase before:flex-1 before:border-t \n before:border-overlay1 before:me-6 after:flex-1 \n after:border-t after:border-overlay1 after:ms-6\">Or</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = form.RegisterForm("").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layout.Global("Register").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -5,12 +5,10 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"projectreshoot/pkg/logging"
|
||||
"projectreshoot/pkg/tmdb"
|
||||
|
||||
"git.haelnorr.com/h/golib/hlog"
|
||||
"git.haelnorr.com/h/golib/tmdb"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -28,7 +26,7 @@ type Config struct {
|
||||
AccessTokenExpiry int64 // Access token expiry in minutes
|
||||
RefreshTokenExpiry int64 // Refresh token expiry in minutes
|
||||
TokenFreshTime int64 // Time for tokens to stay fresh in minutes
|
||||
LogLevel zerolog.Level // Log level for global logging. Defaults to info
|
||||
LogLevel hlog.Level // Log level for global logging. Defaults to info
|
||||
LogOutput string // "file", "console", or "both". Defaults to console
|
||||
LogDir string // Path to create log files
|
||||
TMDBToken string // Read access token for TMDB API
|
||||
@@ -41,7 +39,7 @@ func GetConfig(args map[string]string) (*Config, error) {
|
||||
var (
|
||||
host string
|
||||
port string
|
||||
logLevel zerolog.Level
|
||||
logLevel hlog.Level
|
||||
logOutput string
|
||||
valid bool
|
||||
)
|
||||
@@ -57,9 +55,9 @@ func GetConfig(args map[string]string) (*Config, error) {
|
||||
port = GetEnvDefault("PORT", "3010")
|
||||
}
|
||||
if args["loglevel"] != "" {
|
||||
logLevel = logging.GetLogLevel(args["loglevel"])
|
||||
logLevel = hlog.LogLevel(args["loglevel"])
|
||||
} else {
|
||||
logLevel = logging.GetLogLevel(GetEnvDefault("LOG_LEVEL", "info"))
|
||||
logLevel = hlog.LogLevel(GetEnvDefault("LOG_LEVEL", "info"))
|
||||
}
|
||||
if args["logoutput"] != "" {
|
||||
opts := map[string]string{
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"projectreshoot/internal/models"
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/jwt"
|
||||
|
||||
"git.haelnorr.com/h/golib/jwt"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -58,15 +58,16 @@ func SetTokenCookies(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
config *config.Config,
|
||||
tokenGen *jwt.TokenGenerator,
|
||||
user *models.User,
|
||||
fresh bool,
|
||||
rememberMe bool,
|
||||
) error {
|
||||
at, atexp, err := jwt.GenerateAccessToken(config, user, fresh, rememberMe)
|
||||
at, atexp, err := tokenGen.NewAccess(user.ID, fresh, rememberMe)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "jwt.GenerateAccessToken")
|
||||
}
|
||||
rt, rtexp, err := jwt.GenerateRefreshToken(config, user, rememberMe)
|
||||
rt, rtexp, err := tokenGen.NewRefresh(user.ID, rememberMe)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "jwt.GenerateRefreshToken")
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Returns a database connection handle for the DB
|
||||
func ConnectToDatabase(
|
||||
dbName string,
|
||||
logger *zerolog.Logger,
|
||||
) (*SafeConn, error) {
|
||||
opts := "_journal_mode=WAL&_synchronous=NORMAL&_txlock=IMMEDIATE"
|
||||
file := fmt.Sprintf("file:%s.db?%s", dbName, opts)
|
||||
wconn, err := sql.Open("sqlite3", file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sql.Open (rw)")
|
||||
}
|
||||
wconn.SetMaxOpenConns(1)
|
||||
opts = "_synchronous=NORMAL&mode=ro"
|
||||
file = fmt.Sprintf("file:%s.db?%s", dbName, opts)
|
||||
|
||||
rconn, err := sql.Open("sqlite3", file)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sql.Open (ro)")
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(dbName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "strconv.Atoi")
|
||||
}
|
||||
err = checkDBVersion(rconn, version)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "checkDBVersion")
|
||||
}
|
||||
conn := MakeSafe(wconn, rconn, logger)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Check the database version
|
||||
func checkDBVersion(db *sql.DB, expectVer int) error {
|
||||
query := `SELECT version_id FROM goose_db_version WHERE is_applied = 1
|
||||
ORDER BY version_id DESC LIMIT 1`
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "db.Query")
|
||||
}
|
||||
defer rows.Close()
|
||||
if rows.Next() {
|
||||
var version int
|
||||
err = rows.Scan(&version)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "rows.Scan")
|
||||
}
|
||||
if version != expectVer {
|
||||
return errors.New("Version mismatch")
|
||||
}
|
||||
} else {
|
||||
return errors.New("No version found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type SafeConn struct {
|
||||
wconn *sql.DB
|
||||
rconn *sql.DB
|
||||
readLockCount uint32
|
||||
globalLockStatus uint32
|
||||
globalLockRequested uint32
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
// Make the provided db handle safe and attach a logger to it
|
||||
func MakeSafe(wconn *sql.DB, rconn *sql.DB, logger *zerolog.Logger) *SafeConn {
|
||||
return &SafeConn{wconn: wconn, rconn: rconn, logger: logger}
|
||||
}
|
||||
|
||||
// Attempts to acquire a global lock on the database connection
|
||||
func (conn *SafeConn) acquireGlobalLock() bool {
|
||||
if conn.readLockCount > 0 || conn.globalLockStatus == 1 {
|
||||
return false
|
||||
}
|
||||
conn.globalLockStatus = 1
|
||||
conn.logger.Debug().Uint32("global_lock_status", conn.globalLockStatus).
|
||||
Msg("Global lock acquired")
|
||||
return true
|
||||
}
|
||||
|
||||
// Releases a global lock on the database connection
|
||||
func (conn *SafeConn) releaseGlobalLock() {
|
||||
conn.globalLockStatus = 0
|
||||
conn.logger.Debug().Uint32("global_lock_status", conn.globalLockStatus).
|
||||
Msg("Global lock released")
|
||||
}
|
||||
|
||||
// Acquire a read lock on the connection. Multiple read locks can be acquired
|
||||
// at the same time
|
||||
func (conn *SafeConn) acquireReadLock() bool {
|
||||
if conn.globalLockStatus == 1 || conn.globalLockRequested == 1 {
|
||||
return false
|
||||
}
|
||||
conn.readLockCount += 1
|
||||
conn.logger.Debug().Uint32("read_lock_count", conn.readLockCount).
|
||||
Msg("Read lock acquired")
|
||||
return true
|
||||
}
|
||||
|
||||
// Release a read lock. Decrements read lock count by 1
|
||||
func (conn *SafeConn) releaseReadLock() {
|
||||
conn.readLockCount -= 1
|
||||
conn.logger.Debug().Uint32("read_lock_count", conn.readLockCount).
|
||||
Msg("Read lock released")
|
||||
}
|
||||
|
||||
// Starts a new transaction based on the current context. Will cancel if
|
||||
// the context is closed/cancelled/done
|
||||
func (conn *SafeConn) Begin(ctx context.Context) (*SafeWTX, error) {
|
||||
lockAcquired := make(chan struct{})
|
||||
lockCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-lockCtx.Done():
|
||||
return
|
||||
default:
|
||||
if conn.acquireReadLock() {
|
||||
close(lockAcquired)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-lockAcquired:
|
||||
tx, err := conn.wconn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
conn.releaseReadLock()
|
||||
return nil, err
|
||||
}
|
||||
return &SafeWTX{tx: tx, sc: conn}, nil
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
return nil, errors.New("Transaction time out due to database lock")
|
||||
}
|
||||
}
|
||||
|
||||
// Starts a new READONLY transaction based on the current context. Will cancel if
|
||||
// the context is closed/cancelled/done
|
||||
func (conn *SafeConn) RBegin(ctx context.Context) (*SafeRTX, error) {
|
||||
lockAcquired := make(chan struct{})
|
||||
lockCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-lockCtx.Done():
|
||||
return
|
||||
default:
|
||||
if conn.acquireReadLock() {
|
||||
close(lockAcquired)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-lockAcquired:
|
||||
tx, err := conn.rconn.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
conn.releaseReadLock()
|
||||
return nil, err
|
||||
}
|
||||
return &SafeRTX{tx: tx, sc: conn}, nil
|
||||
case <-ctx.Done():
|
||||
cancel()
|
||||
return nil, errors.New("Transaction time out due to database lock")
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire a global lock, preventing all transactions
|
||||
func (conn *SafeConn) Pause(timeoutAfter time.Duration) {
|
||||
conn.logger.Info().Msg("Attempting to acquire global database lock")
|
||||
conn.globalLockRequested = 1
|
||||
defer func() { conn.globalLockRequested = 0 }()
|
||||
timeout := time.After(timeoutAfter)
|
||||
attempt := 0
|
||||
for {
|
||||
if conn.acquireGlobalLock() {
|
||||
conn.logger.Info().Msg("Global database lock acquired")
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-timeout:
|
||||
conn.logger.Info().Msg("Timeout: Global database lock abandoned")
|
||||
return
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release the global lock
|
||||
func (conn *SafeConn) Resume() {
|
||||
conn.releaseGlobalLock()
|
||||
conn.logger.Info().Msg("Global database lock released")
|
||||
}
|
||||
|
||||
// Close the database connection
|
||||
func (conn *SafeConn) Close() error {
|
||||
conn.logger.Debug().Msg("Acquiring global lock for connection close")
|
||||
conn.acquireGlobalLock()
|
||||
defer conn.releaseGlobalLock()
|
||||
conn.logger.Debug().Msg("Closing database connection")
|
||||
return conn.wconn.Close()
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"projectreshoot/pkg/tests"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSafeConn(t *testing.T) {
|
||||
cfg, err := tests.TestConfig()
|
||||
require.NoError(t, err)
|
||||
logger := tests.NilLogger()
|
||||
ver, err := strconv.ParseInt(cfg.DBName, 10, 0)
|
||||
require.NoError(t, err)
|
||||
wconn, rconn, err := tests.SetupTestDB(ver)
|
||||
require.NoError(t, err)
|
||||
sconn := MakeSafe(wconn, rconn, logger)
|
||||
defer sconn.Close()
|
||||
|
||||
t.Run("Global lock waits for read locks to finish", func(t *testing.T) {
|
||||
tx, err := sconn.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
var requested sync.WaitGroup
|
||||
var engaged sync.WaitGroup
|
||||
requested.Add(1)
|
||||
engaged.Add(1)
|
||||
go func() {
|
||||
requested.Done()
|
||||
sconn.Pause(5 * time.Second)
|
||||
engaged.Done()
|
||||
}()
|
||||
requested.Wait()
|
||||
assert.Equal(t, uint32(0), sconn.globalLockStatus)
|
||||
assert.Equal(t, uint32(1), sconn.globalLockRequested)
|
||||
tx.Commit()
|
||||
engaged.Wait()
|
||||
assert.Equal(t, uint32(1), sconn.globalLockStatus)
|
||||
assert.Equal(t, uint32(0), sconn.globalLockRequested)
|
||||
sconn.Resume()
|
||||
})
|
||||
t.Run("Lock abandons after timeout", func(t *testing.T) {
|
||||
tx, err := sconn.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
sconn.Pause(250 * time.Millisecond)
|
||||
assert.Equal(t, uint32(0), sconn.globalLockStatus)
|
||||
assert.Equal(t, uint32(0), sconn.globalLockRequested)
|
||||
tx.Commit()
|
||||
})
|
||||
t.Run("Pause blocks transactions and resume allows", func(t *testing.T) {
|
||||
tx, err := sconn.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
var requested sync.WaitGroup
|
||||
var engaged sync.WaitGroup
|
||||
requested.Add(1)
|
||||
engaged.Add(1)
|
||||
go func() {
|
||||
requested.Done()
|
||||
sconn.Pause(5 * time.Second)
|
||||
engaged.Done()
|
||||
}()
|
||||
requested.Wait()
|
||||
assert.Equal(t, uint32(0), sconn.globalLockStatus)
|
||||
assert.Equal(t, uint32(1), sconn.globalLockRequested)
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 250*time.Millisecond)
|
||||
defer cancel()
|
||||
_, err = sconn.Begin(ctx)
|
||||
require.Error(t, err)
|
||||
tx.Commit()
|
||||
engaged.Wait()
|
||||
_, err = sconn.Begin(ctx)
|
||||
require.Error(t, err)
|
||||
sconn.Resume()
|
||||
tx, err = sconn.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
tx.Commit()
|
||||
})
|
||||
}
|
||||
func TestSafeTX(t *testing.T) {
|
||||
cfg, err := tests.TestConfig()
|
||||
require.NoError(t, err)
|
||||
logger := tests.NilLogger()
|
||||
ver, err := strconv.ParseInt(cfg.DBName, 10, 0)
|
||||
require.NoError(t, err)
|
||||
wconn, rconn, err := tests.SetupTestDB(ver)
|
||||
require.NoError(t, err)
|
||||
sconn := MakeSafe(wconn, rconn, logger)
|
||||
defer sconn.Close()
|
||||
|
||||
t.Run("Commit releases lock", func(t *testing.T) {
|
||||
tx, err := sconn.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint32(1), sconn.readLockCount)
|
||||
tx.Commit()
|
||||
assert.Equal(t, uint32(0), sconn.readLockCount)
|
||||
})
|
||||
t.Run("Rollback releases lock", func(t *testing.T) {
|
||||
tx, err := sconn.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint32(1), sconn.readLockCount)
|
||||
tx.Rollback()
|
||||
assert.Equal(t, uint32(0), sconn.readLockCount)
|
||||
})
|
||||
t.Run("Multiple RTX can gain read lock", func(t *testing.T) {
|
||||
tx1, err := sconn.RBegin(t.Context())
|
||||
require.NoError(t, err)
|
||||
tx2, err := sconn.RBegin(t.Context())
|
||||
require.NoError(t, err)
|
||||
tx3, err := sconn.RBegin(t.Context())
|
||||
require.NoError(t, err)
|
||||
tx1.Commit()
|
||||
tx2.Commit()
|
||||
tx3.Commit()
|
||||
})
|
||||
t.Run("Lock acquiring times out after timeout", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 250*time.Millisecond)
|
||||
defer cancel()
|
||||
sconn.acquireGlobalLock()
|
||||
defer sconn.releaseGlobalLock()
|
||||
_, err := sconn.Begin(ctx)
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("Lock acquires if lock released", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 250*time.Millisecond)
|
||||
defer cancel()
|
||||
sconn.acquireGlobalLock()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
tx, err := sconn.Begin(ctx)
|
||||
require.NoError(t, err)
|
||||
tx.Commit()
|
||||
wg.Done()
|
||||
}()
|
||||
sconn.releaseGlobalLock()
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
163
pkg/db/safetx.go
163
pkg/db/safetx.go
@@ -1,163 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SafeTX interface {
|
||||
Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
||||
QueryRow(ctx context.Context, query string, args ...interface{}) (*sql.Row, error)
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
// Extends sql.Tx for use with SafeConn
|
||||
type SafeWTX struct {
|
||||
tx *sql.Tx
|
||||
sc *SafeConn
|
||||
}
|
||||
type SafeRTX struct {
|
||||
tx *sql.Tx
|
||||
sc *SafeConn
|
||||
}
|
||||
|
||||
func isWriteOperation(query string) bool {
|
||||
query = strings.TrimSpace(query)
|
||||
query = strings.ToUpper(query)
|
||||
writeOpsRegex := `^(INSERT|UPDATE|DELETE|REPLACE|MERGE|CREATE|DROP|ALTER|TRUNCATE)\s+`
|
||||
re := regexp.MustCompile(writeOpsRegex)
|
||||
return re.MatchString(query)
|
||||
}
|
||||
|
||||
// Query the database inside the transaction
|
||||
func (stx *SafeRTX) Query(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
args ...interface{},
|
||||
) (*sql.Rows, error) {
|
||||
if stx.tx == nil {
|
||||
return nil, errors.New("Cannot query without a transaction")
|
||||
}
|
||||
if isWriteOperation(query) {
|
||||
return nil, errors.New("Cannot query with a write operation")
|
||||
}
|
||||
rows, err := stx.tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.QueryContext")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// Query the database inside the transaction
|
||||
func (stx *SafeWTX) Query(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
args ...interface{},
|
||||
) (*sql.Rows, error) {
|
||||
if stx.tx == nil {
|
||||
return nil, errors.New("Cannot query without a transaction")
|
||||
}
|
||||
if isWriteOperation(query) {
|
||||
return nil, errors.New("Cannot query with a write operation")
|
||||
}
|
||||
rows, err := stx.tx.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.QueryContext")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// Query a row from the database inside the transaction
|
||||
func (stx *SafeRTX) QueryRow(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
args ...interface{},
|
||||
) (*sql.Row, error) {
|
||||
if stx.tx == nil {
|
||||
return nil, errors.New("Cannot query without a transaction")
|
||||
}
|
||||
if isWriteOperation(query) {
|
||||
return nil, errors.New("Cannot query with a write operation")
|
||||
}
|
||||
return stx.tx.QueryRowContext(ctx, query, args...), nil
|
||||
}
|
||||
|
||||
// Query a row from the database inside the transaction
|
||||
func (stx *SafeWTX) QueryRow(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
args ...interface{},
|
||||
) (*sql.Row, error) {
|
||||
if stx.tx == nil {
|
||||
return nil, errors.New("Cannot query without a transaction")
|
||||
}
|
||||
if isWriteOperation(query) {
|
||||
return nil, errors.New("Cannot query with a write operation")
|
||||
}
|
||||
return stx.tx.QueryRowContext(ctx, query, args...), nil
|
||||
}
|
||||
|
||||
// Exec a statement on the database inside the transaction
|
||||
func (stx *SafeWTX) Exec(
|
||||
ctx context.Context,
|
||||
query string,
|
||||
args ...interface{},
|
||||
) (sql.Result, error) {
|
||||
if stx.tx == nil {
|
||||
return nil, errors.New("Cannot exec without a transaction")
|
||||
}
|
||||
res, err := stx.tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.ExecContext")
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Commit the current transaction and release the read lock
|
||||
func (stx *SafeRTX) Commit() error {
|
||||
if stx.tx == nil {
|
||||
return errors.New("Cannot commit without a transaction")
|
||||
}
|
||||
err := stx.tx.Commit()
|
||||
stx.tx = nil
|
||||
stx.sc.releaseReadLock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Commit the current transaction and release the read lock
|
||||
func (stx *SafeWTX) Commit() error {
|
||||
if stx.tx == nil {
|
||||
return errors.New("Cannot commit without a transaction")
|
||||
}
|
||||
err := stx.tx.Commit()
|
||||
stx.tx = nil
|
||||
stx.sc.releaseReadLock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Abort the current transaction, releasing the read lock
|
||||
func (stx *SafeRTX) Rollback() error {
|
||||
if stx.tx == nil {
|
||||
return errors.New("Cannot rollback without a transaction")
|
||||
}
|
||||
err := stx.tx.Rollback()
|
||||
stx.tx = nil
|
||||
stx.sc.releaseReadLock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Abort the current transaction, releasing the read lock
|
||||
func (stx *SafeWTX) Rollback() error {
|
||||
if stx.tx == nil {
|
||||
return errors.New("Cannot rollback without a transaction")
|
||||
}
|
||||
err := stx.tx.Rollback()
|
||||
stx.tx = nil
|
||||
stx.sc.releaseReadLock()
|
||||
return err
|
||||
}
|
||||
@@ -1,282 +1,24 @@
|
||||
/*! tailwindcss v4.0.3 | MIT License | https://tailwindcss.com */
|
||||
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Ubuntu+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap");
|
||||
@layer properties;
|
||||
@layer theme, base, components, utilities;
|
||||
@layer theme {
|
||||
:root, :host {
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
'Courier New', monospace;
|
||||
--color-red-50: oklch(0.971 0.013 17.38);
|
||||
--color-red-100: oklch(0.936 0.032 17.717);
|
||||
--color-red-200: oklch(0.885 0.062 18.334);
|
||||
--color-red-300: oklch(0.808 0.114 19.571);
|
||||
--color-red-400: oklch(0.704 0.191 22.216);
|
||||
--color-red-500: oklch(0.637 0.237 25.331);
|
||||
--color-red-600: oklch(0.577 0.245 27.325);
|
||||
--color-red-700: oklch(0.505 0.213 27.518);
|
||||
--color-red-800: oklch(0.444 0.177 26.899);
|
||||
--color-red-900: oklch(0.396 0.141 25.723);
|
||||
--color-red-950: oklch(0.258 0.092 26.042);
|
||||
--color-orange-50: oklch(0.98 0.016 73.684);
|
||||
--color-orange-100: oklch(0.954 0.038 75.164);
|
||||
--color-orange-200: oklch(0.901 0.076 70.697);
|
||||
--color-orange-300: oklch(0.837 0.128 66.29);
|
||||
--color-orange-400: oklch(0.75 0.183 55.934);
|
||||
--color-orange-500: oklch(0.705 0.213 47.604);
|
||||
--color-orange-600: oklch(0.646 0.222 41.116);
|
||||
--color-orange-700: oklch(0.553 0.195 38.402);
|
||||
--color-orange-800: oklch(0.47 0.157 37.304);
|
||||
--color-orange-900: oklch(0.408 0.123 38.172);
|
||||
--color-orange-950: oklch(0.266 0.079 36.259);
|
||||
--color-amber-50: oklch(0.987 0.022 95.277);
|
||||
--color-amber-100: oklch(0.962 0.059 95.617);
|
||||
--color-amber-200: oklch(0.924 0.12 95.746);
|
||||
--color-amber-300: oklch(0.879 0.169 91.605);
|
||||
--color-amber-400: oklch(0.828 0.189 84.429);
|
||||
--color-amber-500: oklch(0.769 0.188 70.08);
|
||||
--color-amber-600: oklch(0.666 0.179 58.318);
|
||||
--color-amber-700: oklch(0.555 0.163 48.998);
|
||||
--color-amber-800: oklch(0.473 0.137 46.201);
|
||||
--color-amber-900: oklch(0.414 0.112 45.904);
|
||||
--color-amber-950: oklch(0.279 0.077 45.635);
|
||||
--color-yellow-50: oklch(0.987 0.026 102.212);
|
||||
--color-yellow-100: oklch(0.973 0.071 103.193);
|
||||
--color-yellow-200: oklch(0.945 0.129 101.54);
|
||||
--color-yellow-300: oklch(0.905 0.182 98.111);
|
||||
--color-yellow-400: oklch(0.852 0.199 91.936);
|
||||
--color-yellow-500: oklch(0.795 0.184 86.047);
|
||||
--color-yellow-600: oklch(0.681 0.162 75.834);
|
||||
--color-yellow-700: oklch(0.554 0.135 66.442);
|
||||
--color-yellow-800: oklch(0.476 0.114 61.907);
|
||||
--color-yellow-900: oklch(0.421 0.095 57.708);
|
||||
--color-yellow-950: oklch(0.286 0.066 53.813);
|
||||
--color-lime-50: oklch(0.986 0.031 120.757);
|
||||
--color-lime-100: oklch(0.967 0.067 122.328);
|
||||
--color-lime-200: oklch(0.938 0.127 124.321);
|
||||
--color-lime-300: oklch(0.897 0.196 126.665);
|
||||
--color-lime-400: oklch(0.841 0.238 128.85);
|
||||
--color-lime-500: oklch(0.768 0.233 130.85);
|
||||
--color-lime-600: oklch(0.648 0.2 131.684);
|
||||
--color-lime-700: oklch(0.532 0.157 131.589);
|
||||
--color-lime-800: oklch(0.453 0.124 130.933);
|
||||
--color-lime-900: oklch(0.405 0.101 131.063);
|
||||
--color-lime-950: oklch(0.274 0.072 132.109);
|
||||
--color-green-50: oklch(0.982 0.018 155.826);
|
||||
--color-green-100: oklch(0.962 0.044 156.743);
|
||||
--color-green-200: oklch(0.925 0.084 155.995);
|
||||
--color-green-300: oklch(0.871 0.15 154.449);
|
||||
--color-green-400: oklch(0.792 0.209 151.711);
|
||||
--color-green-500: oklch(0.723 0.219 149.579);
|
||||
--color-green-600: oklch(0.627 0.194 149.214);
|
||||
--color-green-700: oklch(0.527 0.154 150.069);
|
||||
--color-green-800: oklch(0.448 0.119 151.328);
|
||||
--color-green-900: oklch(0.393 0.095 152.535);
|
||||
--color-green-950: oklch(0.266 0.065 152.934);
|
||||
--color-emerald-50: oklch(0.979 0.021 166.113);
|
||||
--color-emerald-100: oklch(0.95 0.052 163.051);
|
||||
--color-emerald-200: oklch(0.905 0.093 164.15);
|
||||
--color-emerald-300: oklch(0.845 0.143 164.978);
|
||||
--color-emerald-400: oklch(0.765 0.177 163.223);
|
||||
--color-emerald-500: oklch(0.696 0.17 162.48);
|
||||
--color-emerald-600: oklch(0.596 0.145 163.225);
|
||||
--color-emerald-700: oklch(0.508 0.118 165.612);
|
||||
--color-emerald-800: oklch(0.432 0.095 166.913);
|
||||
--color-emerald-900: oklch(0.378 0.077 168.94);
|
||||
--color-emerald-950: oklch(0.262 0.051 172.552);
|
||||
--color-teal-50: oklch(0.984 0.014 180.72);
|
||||
--color-teal-100: oklch(0.953 0.051 180.801);
|
||||
--color-teal-200: oklch(0.91 0.096 180.426);
|
||||
--color-teal-300: oklch(0.855 0.138 181.071);
|
||||
--color-teal-400: oklch(0.777 0.152 181.912);
|
||||
--color-teal-500: oklch(0.704 0.14 182.503);
|
||||
--color-teal-600: oklch(0.6 0.118 184.704);
|
||||
--color-teal-700: oklch(0.511 0.096 186.391);
|
||||
--color-teal-800: oklch(0.437 0.078 188.216);
|
||||
--color-teal-900: oklch(0.386 0.063 188.416);
|
||||
--color-teal-950: oklch(0.277 0.046 192.524);
|
||||
--color-cyan-50: oklch(0.984 0.019 200.873);
|
||||
--color-cyan-100: oklch(0.956 0.045 203.388);
|
||||
--color-cyan-200: oklch(0.917 0.08 205.041);
|
||||
--color-cyan-300: oklch(0.865 0.127 207.078);
|
||||
--color-cyan-400: oklch(0.789 0.154 211.53);
|
||||
--color-cyan-500: oklch(0.715 0.143 215.221);
|
||||
--color-cyan-600: oklch(0.609 0.126 221.723);
|
||||
--color-cyan-700: oklch(0.52 0.105 223.128);
|
||||
--color-cyan-800: oklch(0.45 0.085 224.283);
|
||||
--color-cyan-900: oklch(0.398 0.07 227.392);
|
||||
--color-cyan-950: oklch(0.302 0.056 229.695);
|
||||
--color-sky-50: oklch(0.977 0.013 236.62);
|
||||
--color-sky-100: oklch(0.951 0.026 236.824);
|
||||
--color-sky-200: oklch(0.901 0.058 230.902);
|
||||
--color-sky-300: oklch(0.828 0.111 230.318);
|
||||
--color-sky-400: oklch(0.746 0.16 232.661);
|
||||
--color-sky-500: oklch(0.685 0.169 237.323);
|
||||
--color-sky-600: oklch(0.588 0.158 241.966);
|
||||
--color-sky-700: oklch(0.5 0.134 242.749);
|
||||
--color-sky-800: oklch(0.443 0.11 240.79);
|
||||
--color-sky-900: oklch(0.391 0.09 240.876);
|
||||
--color-sky-950: oklch(0.293 0.066 243.157);
|
||||
--color-blue-50: oklch(0.97 0.014 254.604);
|
||||
--color-blue-100: oklch(0.932 0.032 255.585);
|
||||
--color-blue-200: oklch(0.882 0.059 254.128);
|
||||
--color-blue-300: oklch(0.809 0.105 251.813);
|
||||
--color-blue-400: oklch(0.707 0.165 254.624);
|
||||
--color-blue-500: oklch(0.623 0.214 259.815);
|
||||
--color-blue-600: oklch(0.546 0.245 262.881);
|
||||
--color-blue-700: oklch(0.488 0.243 264.376);
|
||||
--color-blue-800: oklch(0.424 0.199 265.638);
|
||||
--color-blue-900: oklch(0.379 0.146 265.522);
|
||||
--color-blue-950: oklch(0.282 0.091 267.935);
|
||||
--color-indigo-50: oklch(0.962 0.018 272.314);
|
||||
--color-indigo-100: oklch(0.93 0.034 272.788);
|
||||
--color-indigo-200: oklch(0.87 0.065 274.039);
|
||||
--color-indigo-300: oklch(0.785 0.115 274.713);
|
||||
--color-indigo-400: oklch(0.673 0.182 276.935);
|
||||
--color-indigo-500: oklch(0.585 0.233 277.117);
|
||||
--color-indigo-600: oklch(0.511 0.262 276.966);
|
||||
--color-indigo-700: oklch(0.457 0.24 277.023);
|
||||
--color-indigo-800: oklch(0.398 0.195 277.366);
|
||||
--color-indigo-900: oklch(0.359 0.144 278.697);
|
||||
--color-indigo-950: oklch(0.257 0.09 281.288);
|
||||
--color-violet-50: oklch(0.969 0.016 293.756);
|
||||
--color-violet-100: oklch(0.943 0.029 294.588);
|
||||
--color-violet-200: oklch(0.894 0.057 293.283);
|
||||
--color-violet-300: oklch(0.811 0.111 293.571);
|
||||
--color-violet-400: oklch(0.702 0.183 293.541);
|
||||
--color-violet-500: oklch(0.606 0.25 292.717);
|
||||
--color-violet-600: oklch(0.541 0.281 293.009);
|
||||
--color-violet-700: oklch(0.491 0.27 292.581);
|
||||
--color-violet-800: oklch(0.432 0.232 292.759);
|
||||
--color-violet-900: oklch(0.38 0.189 293.745);
|
||||
--color-violet-950: oklch(0.283 0.141 291.089);
|
||||
--color-purple-50: oklch(0.977 0.014 308.299);
|
||||
--color-purple-100: oklch(0.946 0.033 307.174);
|
||||
--color-purple-200: oklch(0.902 0.063 306.703);
|
||||
--color-purple-300: oklch(0.827 0.119 306.383);
|
||||
--color-purple-400: oklch(0.714 0.203 305.504);
|
||||
--color-purple-500: oklch(0.627 0.265 303.9);
|
||||
--color-purple-600: oklch(0.558 0.288 302.321);
|
||||
--color-purple-700: oklch(0.496 0.265 301.924);
|
||||
--color-purple-800: oklch(0.438 0.218 303.724);
|
||||
--color-purple-900: oklch(0.381 0.176 304.987);
|
||||
--color-purple-950: oklch(0.291 0.149 302.717);
|
||||
--color-fuchsia-50: oklch(0.977 0.017 320.058);
|
||||
--color-fuchsia-100: oklch(0.952 0.037 318.852);
|
||||
--color-fuchsia-200: oklch(0.903 0.076 319.62);
|
||||
--color-fuchsia-300: oklch(0.833 0.145 321.434);
|
||||
--color-fuchsia-400: oklch(0.74 0.238 322.16);
|
||||
--color-fuchsia-500: oklch(0.667 0.295 322.15);
|
||||
--color-fuchsia-600: oklch(0.591 0.293 322.896);
|
||||
--color-fuchsia-700: oklch(0.518 0.253 323.949);
|
||||
--color-fuchsia-800: oklch(0.452 0.211 324.591);
|
||||
--color-fuchsia-900: oklch(0.401 0.17 325.612);
|
||||
--color-fuchsia-950: oklch(0.293 0.136 325.661);
|
||||
--color-pink-50: oklch(0.971 0.014 343.198);
|
||||
--color-pink-100: oklch(0.948 0.028 342.258);
|
||||
--color-pink-200: oklch(0.899 0.061 343.231);
|
||||
--color-pink-300: oklch(0.823 0.12 346.018);
|
||||
--color-pink-400: oklch(0.718 0.202 349.761);
|
||||
--color-pink-500: oklch(0.656 0.241 354.308);
|
||||
--color-pink-600: oklch(0.592 0.249 0.584);
|
||||
--color-pink-700: oklch(0.525 0.223 3.958);
|
||||
--color-pink-800: oklch(0.459 0.187 3.815);
|
||||
--color-pink-900: oklch(0.408 0.153 2.432);
|
||||
--color-pink-950: oklch(0.284 0.109 3.907);
|
||||
--color-rose-50: oklch(0.969 0.015 12.422);
|
||||
--color-rose-100: oklch(0.941 0.03 12.58);
|
||||
--color-rose-200: oklch(0.892 0.058 10.001);
|
||||
--color-rose-300: oklch(0.81 0.117 11.638);
|
||||
--color-rose-400: oklch(0.712 0.194 13.428);
|
||||
--color-rose-500: oklch(0.645 0.246 16.439);
|
||||
--color-rose-600: oklch(0.586 0.253 17.585);
|
||||
--color-rose-700: oklch(0.514 0.222 16.935);
|
||||
--color-rose-800: oklch(0.455 0.188 13.697);
|
||||
--color-rose-900: oklch(0.41 0.159 10.272);
|
||||
--color-rose-950: oklch(0.271 0.105 12.094);
|
||||
--color-slate-50: oklch(0.984 0.003 247.858);
|
||||
--color-slate-100: oklch(0.968 0.007 247.896);
|
||||
--color-slate-200: oklch(0.929 0.013 255.508);
|
||||
--color-slate-300: oklch(0.869 0.022 252.894);
|
||||
--color-slate-400: oklch(0.704 0.04 256.788);
|
||||
--color-slate-500: oklch(0.554 0.046 257.417);
|
||||
--color-slate-600: oklch(0.446 0.043 257.281);
|
||||
--color-slate-700: oklch(0.372 0.044 257.287);
|
||||
--color-slate-800: oklch(0.279 0.041 260.031);
|
||||
--color-slate-900: oklch(0.208 0.042 265.755);
|
||||
--color-slate-950: oklch(0.129 0.042 264.695);
|
||||
--color-gray-50: oklch(0.985 0.002 247.839);
|
||||
--color-gray-100: oklch(0.967 0.003 264.542);
|
||||
--color-gray-200: oklch(0.928 0.006 264.531);
|
||||
--color-gray-300: oklch(0.872 0.01 258.338);
|
||||
--color-gray-400: oklch(0.707 0.022 261.325);
|
||||
--color-gray-500: oklch(0.551 0.027 264.364);
|
||||
--color-gray-600: oklch(0.446 0.03 256.802);
|
||||
--color-gray-700: oklch(0.373 0.034 259.733);
|
||||
--color-gray-800: oklch(0.278 0.033 256.848);
|
||||
--color-gray-900: oklch(0.21 0.034 264.665);
|
||||
--color-gray-950: oklch(0.13 0.028 261.692);
|
||||
--color-zinc-50: oklch(0.985 0 0);
|
||||
--color-zinc-100: oklch(0.967 0.001 286.375);
|
||||
--color-zinc-200: oklch(0.92 0.004 286.32);
|
||||
--color-zinc-300: oklch(0.871 0.006 286.286);
|
||||
--color-zinc-400: oklch(0.705 0.015 286.067);
|
||||
--color-zinc-500: oklch(0.552 0.016 285.938);
|
||||
--color-zinc-600: oklch(0.442 0.017 285.786);
|
||||
--color-zinc-700: oklch(0.37 0.013 285.805);
|
||||
--color-zinc-800: oklch(0.274 0.006 286.033);
|
||||
--color-zinc-900: oklch(0.21 0.006 285.885);
|
||||
--color-zinc-950: oklch(0.141 0.005 285.823);
|
||||
--color-neutral-50: oklch(0.985 0 0);
|
||||
--color-neutral-100: oklch(0.97 0 0);
|
||||
--color-neutral-200: oklch(0.922 0 0);
|
||||
--color-neutral-300: oklch(0.87 0 0);
|
||||
--color-neutral-400: oklch(0.708 0 0);
|
||||
--color-neutral-500: oklch(0.556 0 0);
|
||||
--color-neutral-600: oklch(0.439 0 0);
|
||||
--color-neutral-700: oklch(0.371 0 0);
|
||||
--color-neutral-800: oklch(0.269 0 0);
|
||||
--color-neutral-900: oklch(0.205 0 0);
|
||||
--color-neutral-950: oklch(0.145 0 0);
|
||||
--color-stone-50: oklch(0.985 0.001 106.423);
|
||||
--color-stone-100: oklch(0.97 0.001 106.424);
|
||||
--color-stone-200: oklch(0.923 0.003 48.717);
|
||||
--color-stone-300: oklch(0.869 0.005 56.366);
|
||||
--color-stone-400: oklch(0.709 0.01 56.259);
|
||||
--color-stone-500: oklch(0.553 0.013 58.071);
|
||||
--color-stone-600: oklch(0.444 0.011 73.639);
|
||||
--color-stone-700: oklch(0.374 0.01 67.558);
|
||||
--color-stone-800: oklch(0.268 0.007 34.298);
|
||||
--color-stone-900: oklch(0.216 0.006 56.043);
|
||||
--color-stone-950: oklch(0.147 0.004 49.25);
|
||||
--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--spacing: 0.25rem;
|
||||
--breakpoint-sm: 40rem;
|
||||
--breakpoint-md: 48rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--breakpoint-2xl: 96rem;
|
||||
--container-3xs: 16rem;
|
||||
--container-2xs: 18rem;
|
||||
--container-xs: 20rem;
|
||||
--container-sm: 24rem;
|
||||
--container-md: 28rem;
|
||||
--container-lg: 32rem;
|
||||
--container-xl: 36rem;
|
||||
--container-2xl: 42rem;
|
||||
--container-3xl: 48rem;
|
||||
--container-4xl: 56rem;
|
||||
--container-5xl: 64rem;
|
||||
--container-6xl: 72rem;
|
||||
--container-7xl: 80rem;
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
--text-sm: 0.875rem;
|
||||
--text-sm--line-height: calc(1.25 / 0.875);
|
||||
--text-base: 1rem;
|
||||
--text-base--line-height: calc(1.5 / 1);
|
||||
--text-lg: 1.125rem;
|
||||
--text-lg--line-height: calc(1.75 / 1.125);
|
||||
--text-xl: 1.25rem;
|
||||
@@ -287,115 +29,24 @@
|
||||
--text-3xl--line-height: calc(2.25 / 1.875);
|
||||
--text-4xl: 2.25rem;
|
||||
--text-4xl--line-height: calc(2.5 / 2.25);
|
||||
--text-5xl: 3rem;
|
||||
--text-5xl--line-height: 1;
|
||||
--text-6xl: 3.75rem;
|
||||
--text-6xl--line-height: 1;
|
||||
--text-7xl: 4.5rem;
|
||||
--text-7xl--line-height: 1;
|
||||
--text-8xl: 6rem;
|
||||
--text-8xl--line-height: 1;
|
||||
--text-9xl: 8rem;
|
||||
--text-9xl--line-height: 1;
|
||||
--font-weight-thin: 100;
|
||||
--font-weight-extralight: 200;
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extrabold: 800;
|
||||
--font-weight-black: 900;
|
||||
--tracking-tighter: -0.05em;
|
||||
--tracking-tight: -0.025em;
|
||||
--tracking-normal: 0em;
|
||||
--tracking-wide: 0.025em;
|
||||
--tracking-wider: 0.05em;
|
||||
--tracking-widest: 0.1em;
|
||||
--leading-tight: 1.25;
|
||||
--leading-snug: 1.375;
|
||||
--leading-normal: 1.5;
|
||||
--leading-relaxed: 1.625;
|
||||
--leading-loose: 2;
|
||||
--radius-xs: 0.125rem;
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
--radius-2xl: 1rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
--radius-4xl: 2rem;
|
||||
--shadow-2xs: 0 1px rgb(0 0 0 / 0.05);
|
||||
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05);
|
||||
--inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05);
|
||||
--inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);
|
||||
--drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05);
|
||||
--drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);
|
||||
--drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12);
|
||||
--drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15);
|
||||
--drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1);
|
||||
--drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15);
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--animate-spin: spin 1s linear infinite;
|
||||
--animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
--animate-bounce: bounce 1s infinite;
|
||||
--blur-xs: 4px;
|
||||
--blur-sm: 8px;
|
||||
--blur-md: 12px;
|
||||
--blur-lg: 16px;
|
||||
--blur-xl: 24px;
|
||||
--blur-2xl: 40px;
|
||||
--blur-3xl: 64px;
|
||||
--perspective-dramatic: 100px;
|
||||
--perspective-near: 300px;
|
||||
--perspective-normal: 500px;
|
||||
--perspective-midrange: 800px;
|
||||
--perspective-distant: 1200px;
|
||||
--aspect-video: 16 / 9;
|
||||
--default-transition-duration: 150ms;
|
||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-font-family: var(--font-sans);
|
||||
--default-font-feature-settings: var(--font-sans--font-feature-settings);
|
||||
--default-font-variation-settings: var(--font-sans--font-variation-settings);
|
||||
--default-mono-font-family: var(--font-mono);
|
||||
--default-mono-font-feature-settings: var(--font-mono--font-feature-settings);
|
||||
--default-mono-font-variation-settings: var(--font-mono--font-variation-settings);
|
||||
--color-rosewater: var(--rosewater);
|
||||
--color-flamingo: var(--flamingo);
|
||||
--color-pink: var(--pink);
|
||||
--color-mauve: var(--mauve);
|
||||
--color-red: var(--red);
|
||||
--color-dark-red: var(--dark-red);
|
||||
--color-maroon: var(--maroon);
|
||||
--color-peach: var(--peach);
|
||||
--color-yellow: var(--yellow);
|
||||
--color-green: var(--green);
|
||||
--color-teal: var(--teal);
|
||||
--color-sky: var(--sky);
|
||||
--color-sapphire: var(--sapphire);
|
||||
--color-blue: var(--blue);
|
||||
--color-lavender: var(--lavender);
|
||||
--color-text: var(--text);
|
||||
--color-subtext1: var(--subtext1);
|
||||
--color-subtext0: var(--subtext0);
|
||||
--color-overlay2: var(--overlay2);
|
||||
--color-overlay1: var(--overlay1);
|
||||
--color-overlay0: var(--overlay0);
|
||||
--color-surface2: var(--surface2);
|
||||
--color-surface1: var(--surface1);
|
||||
--color-surface0: var(--surface0);
|
||||
--color-base: var(--base);
|
||||
--color-mantle: var(--mantle);
|
||||
--color-crust: var(--crust);
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
@@ -409,14 +60,11 @@
|
||||
line-height: 1.5;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
tab-size: 4;
|
||||
font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' );
|
||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
|
||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
body {
|
||||
line-height: inherit;
|
||||
}
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
@@ -439,7 +87,7 @@
|
||||
font-weight: bolder;
|
||||
}
|
||||
code, kbd, samp, pre {
|
||||
font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace );
|
||||
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
|
||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
||||
font-size: 1em;
|
||||
@@ -505,7 +153,14 @@
|
||||
}
|
||||
::placeholder {
|
||||
opacity: 1;
|
||||
color: color-mix(in oklab, currentColor 50%, transparent);
|
||||
}
|
||||
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
||||
::placeholder {
|
||||
color: currentcolor;
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, currentcolor 50%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
@@ -526,6 +181,9 @@
|
||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
||||
padding-block: 0;
|
||||
}
|
||||
::-webkit-calendar-picker-indicator {
|
||||
line-height: 1;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -553,7 +211,7 @@
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
@@ -701,7 +359,7 @@
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
.aspect-\[2\/3\] {
|
||||
.aspect-2\/3 {
|
||||
aspect-ratio: 2/3;
|
||||
}
|
||||
.size-5 {
|
||||
@@ -799,7 +457,7 @@
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
|
||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
@@ -837,12 +495,6 @@
|
||||
.gap-8 {
|
||||
gap: calc(var(--spacing) * 8);
|
||||
}
|
||||
.gap-x-1 {
|
||||
column-gap: calc(var(--spacing) * 1);
|
||||
}
|
||||
.gap-x-2 {
|
||||
column-gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
.space-y-1 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
@@ -857,8 +509,11 @@
|
||||
margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.gap-y-4 {
|
||||
row-gap: calc(var(--spacing) * 4);
|
||||
.gap-x-1 {
|
||||
column-gap: calc(var(--spacing) * 1);
|
||||
}
|
||||
.gap-x-2 {
|
||||
column-gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
.space-x-2 {
|
||||
:where(& > :not(:last-child)) {
|
||||
@@ -874,6 +529,9 @@
|
||||
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
}
|
||||
.gap-y-4 {
|
||||
row-gap: calc(var(--spacing) * 4);
|
||||
}
|
||||
.divide-y {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
@@ -965,7 +623,10 @@
|
||||
background-color: var(--overlay0);
|
||||
}
|
||||
.bg-overlay0\/55 {
|
||||
background-color: color-mix(in oklab, var(--overlay0) 55%, transparent);
|
||||
background-color: var(--overlay0);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--overlay0) 55%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-overlay2 {
|
||||
background-color: var(--overlay2);
|
||||
@@ -1161,10 +822,13 @@
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-black {
|
||||
--tw-shadow-color: var(--color-black);
|
||||
--tw-shadow-color: #000;
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
--tw-shadow-color: color-mix(in oklab, var(--color-black) var(--tw-shadow-alpha), transparent);
|
||||
}
|
||||
}
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
@@ -1245,7 +909,10 @@
|
||||
.hover\:bg-blue\/75 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: color-mix(in oklab, var(--blue) 75%, transparent);
|
||||
background-color: var(--blue);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--blue) 75%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1259,7 +926,10 @@
|
||||
.hover\:bg-green\/75 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: color-mix(in oklab, var(--green) 75%, transparent);
|
||||
background-color: var(--green);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--green) 75%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1273,21 +943,30 @@
|
||||
.hover\:bg-mauve\/75 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: color-mix(in oklab, var(--mauve) 75%, transparent);
|
||||
background-color: var(--mauve);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--mauve) 75%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-red\/25 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: color-mix(in oklab, var(--red) 25%, transparent);
|
||||
background-color: var(--red);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--red) 25%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.hover\:bg-sapphire\/75 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: color-mix(in oklab, var(--sapphire) 75%, transparent);
|
||||
background-color: var(--sapphire);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--sapphire) 75%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1308,7 +987,10 @@
|
||||
.hover\:bg-teal\/75 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
background-color: color-mix(in oklab, var(--teal) 75%, transparent);
|
||||
background-color: var(--teal);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--teal) 75%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1322,7 +1004,10 @@
|
||||
.hover\:text-overlay2\/75 {
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: color-mix(in oklab, var(--overlay2) 75%, transparent);
|
||||
color: var(--overlay2);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
color: color-mix(in oklab, var(--overlay2) 75%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1359,7 +1044,7 @@
|
||||
}
|
||||
.focus\:ring-2 {
|
||||
&:focus {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
@@ -1391,12 +1076,18 @@
|
||||
}
|
||||
.disabled\:bg-blue\/60 {
|
||||
&:disabled {
|
||||
background-color: color-mix(in oklab, var(--blue) 60%, transparent);
|
||||
background-color: var(--blue);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--blue) 60%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled\:bg-green\/60 {
|
||||
&:disabled {
|
||||
background-color: color-mix(in oklab, var(--green) 60%, transparent);
|
||||
background-color: var(--green);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--green) 60%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled\:opacity-50 {
|
||||
@@ -1745,32 +1436,6 @@
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes ping {
|
||||
75%, 100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(-25%);
|
||||
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||
}
|
||||
50% {
|
||||
transform: none;
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
@property --tw-translate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
@@ -1789,27 +1454,22 @@
|
||||
@property --tw-rotate-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateX(0);
|
||||
}
|
||||
@property --tw-rotate-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateY(0);
|
||||
}
|
||||
@property --tw-rotate-z {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: rotateZ(0);
|
||||
}
|
||||
@property --tw-skew-x {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: skewX(0);
|
||||
}
|
||||
@property --tw-skew-y {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: skewY(0);
|
||||
}
|
||||
@property --tw-space-y-reverse {
|
||||
syntax: "*";
|
||||
@@ -1852,6 +1512,11 @@
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-shadow-alpha {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 100%;
|
||||
}
|
||||
@property --tw-inset-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
@@ -1861,6 +1526,11 @@
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-inset-shadow-alpha {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 100%;
|
||||
}
|
||||
@property --tw-ring-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
@@ -1911,3 +1581,41 @@
|
||||
initial-value: "";
|
||||
inherits: false;
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-translate-z: 0;
|
||||
--tw-rotate-x: initial;
|
||||
--tw-rotate-y: initial;
|
||||
--tw-rotate-z: initial;
|
||||
--tw-skew-x: initial;
|
||||
--tw-skew-y: initial;
|
||||
--tw-space-y-reverse: 0;
|
||||
--tw-space-x-reverse: 0;
|
||||
--tw-divide-y-reverse: 0;
|
||||
--tw-border-style: solid;
|
||||
--tw-leading: initial;
|
||||
--tw-font-weight: initial;
|
||||
--tw-tracking: initial;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-color: initial;
|
||||
--tw-shadow-alpha: 100%;
|
||||
--tw-inset-shadow: 0 0 #0000;
|
||||
--tw-inset-shadow-color: initial;
|
||||
--tw-inset-shadow-alpha: 100%;
|
||||
--tw-ring-color: initial;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-inset-ring-color: initial;
|
||||
--tw-inset-ring-shadow: 0 0 #0000;
|
||||
--tw-ring-inset: initial;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-duration: initial;
|
||||
--tw-ease: initial;
|
||||
--tw-content: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"projectreshoot/internal/models"
|
||||
"projectreshoot/pkg/config"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Generates an access token for the provided user
|
||||
func GenerateAccessToken(
|
||||
config *config.Config,
|
||||
user *models.User,
|
||||
fresh bool,
|
||||
rememberMe bool,
|
||||
) (tokenStr string, exp int64, err error) {
|
||||
issuedAt := time.Now().Unix()
|
||||
expiresAt := issuedAt + (config.AccessTokenExpiry * 60)
|
||||
var freshExpiresAt int64
|
||||
if fresh {
|
||||
freshExpiresAt = issuedAt + (config.TokenFreshTime * 60)
|
||||
} else {
|
||||
freshExpiresAt = issuedAt
|
||||
}
|
||||
var ttl string
|
||||
if rememberMe {
|
||||
ttl = "exp"
|
||||
} else {
|
||||
ttl = "session"
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
|
||||
jwt.MapClaims{
|
||||
"iss": config.TrustedHost,
|
||||
"scope": "access",
|
||||
"ttl": ttl,
|
||||
"jti": uuid.New(),
|
||||
"iat": issuedAt,
|
||||
"exp": expiresAt,
|
||||
"fresh": freshExpiresAt,
|
||||
"sub": user.ID,
|
||||
})
|
||||
|
||||
signedToken, err := token.SignedString([]byte(config.SecretKey))
|
||||
if err != nil {
|
||||
return "", 0, errors.Wrap(err, "token.SignedString")
|
||||
}
|
||||
return signedToken, expiresAt, nil
|
||||
}
|
||||
|
||||
// Generates a refresh token for the provided user
|
||||
func GenerateRefreshToken(
|
||||
config *config.Config,
|
||||
user *models.User,
|
||||
rememberMe bool,
|
||||
) (tokenStr string, exp int64, err error) {
|
||||
issuedAt := time.Now().Unix()
|
||||
expiresAt := issuedAt + (config.RefreshTokenExpiry * 60)
|
||||
var ttl string
|
||||
if rememberMe {
|
||||
ttl = "exp"
|
||||
} else {
|
||||
ttl = "session"
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
|
||||
jwt.MapClaims{
|
||||
"iss": config.TrustedHost,
|
||||
"scope": "refresh",
|
||||
"ttl": ttl,
|
||||
"jti": uuid.New(),
|
||||
"iat": issuedAt,
|
||||
"exp": expiresAt,
|
||||
"sub": user.ID,
|
||||
})
|
||||
|
||||
signedToken, err := token.SignedString([]byte(config.SecretKey))
|
||||
if err != nil {
|
||||
return "", 0, errors.Wrap(err, "token.SignedString")
|
||||
}
|
||||
return signedToken, expiresAt, nil
|
||||
}
|
||||
268
pkg/jwt/parse.go
268
pkg/jwt/parse.go
@@ -1,268 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"projectreshoot/pkg/config"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Parse an access token and return a struct with all the claims. Does validation on
|
||||
// all the claims, including checking if it is expired, has a valid issuer, and
|
||||
// has the correct scope.
|
||||
func ParseAccessToken(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx db.SafeTX,
|
||||
tokenString string,
|
||||
) (*AccessToken, error) {
|
||||
if tokenString == "" {
|
||||
return nil, errors.New("Access token string not provided")
|
||||
}
|
||||
claims, err := parseToken(config.SecretKey, tokenString)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parseToken")
|
||||
}
|
||||
expiry, err := checkTokenExpired(claims["exp"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "checkTokenExpired")
|
||||
}
|
||||
issuer, err := checkTokenIssuer(config.TrustedHost, claims["iss"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "checkTokenIssuer")
|
||||
}
|
||||
ttl, err := getTokenTTL(claims["ttl"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getTokenTTL")
|
||||
}
|
||||
scope, err := getTokenScope(claims["scope"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getTokenScope")
|
||||
}
|
||||
if scope != "access" {
|
||||
return nil, errors.New("Token is not an Access token")
|
||||
}
|
||||
issuedAt, err := getIssuedTime(claims["iat"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getIssuedTime")
|
||||
}
|
||||
subject, err := getTokenSubject(claims["sub"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getTokenSubject")
|
||||
}
|
||||
fresh, err := getFreshTime(claims["fresh"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getFreshTime")
|
||||
}
|
||||
jti, err := getTokenJTI(claims["jti"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getTokenJTI")
|
||||
}
|
||||
|
||||
token := &AccessToken{
|
||||
ISS: issuer,
|
||||
TTL: ttl,
|
||||
EXP: expiry,
|
||||
IAT: issuedAt,
|
||||
SUB: subject,
|
||||
Fresh: fresh,
|
||||
JTI: jti,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
valid, err := CheckTokenNotRevoked(ctx, tx, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "CheckTokenNotRevoked")
|
||||
}
|
||||
if !valid {
|
||||
return nil, errors.New("Token has been revoked")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Parse a refresh token and return a struct with all the claims. Does validation on
|
||||
// all the claims, including checking if it is expired, has a valid issuer, and
|
||||
// has the correct scope.
|
||||
func ParseRefreshToken(
|
||||
config *config.Config,
|
||||
ctx context.Context,
|
||||
tx db.SafeTX,
|
||||
tokenString string,
|
||||
) (*RefreshToken, error) {
|
||||
if tokenString == "" {
|
||||
return nil, errors.New("Refresh token string not provided")
|
||||
}
|
||||
claims, err := parseToken(config.SecretKey, tokenString)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parseToken")
|
||||
}
|
||||
expiry, err := checkTokenExpired(claims["exp"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "checkTokenExpired")
|
||||
}
|
||||
issuer, err := checkTokenIssuer(config.TrustedHost, claims["iss"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "checkTokenIssuer")
|
||||
}
|
||||
ttl, err := getTokenTTL(claims["ttl"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getTokenTTL")
|
||||
}
|
||||
scope, err := getTokenScope(claims["scope"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getTokenScope")
|
||||
}
|
||||
if scope != "refresh" {
|
||||
return nil, errors.New("Token is not an Refresh token")
|
||||
}
|
||||
issuedAt, err := getIssuedTime(claims["iat"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getIssuedTime")
|
||||
}
|
||||
subject, err := getTokenSubject(claims["sub"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getTokenSubject")
|
||||
}
|
||||
jti, err := getTokenJTI(claims["jti"])
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getTokenJTI")
|
||||
}
|
||||
|
||||
token := &RefreshToken{
|
||||
ISS: issuer,
|
||||
TTL: ttl,
|
||||
EXP: expiry,
|
||||
IAT: issuedAt,
|
||||
SUB: subject,
|
||||
JTI: jti,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
valid, err := CheckTokenNotRevoked(ctx, tx, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "CheckTokenNotRevoked")
|
||||
}
|
||||
if !valid {
|
||||
return nil, errors.New("Token has been revoked")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Parse a token, validating its signing sigature and returning the claims
|
||||
func parseToken(secretKey string, tokenString string) (jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(secretKey), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "jwt.Parse")
|
||||
}
|
||||
// Token decoded, parse the claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("Failed to parse claims")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// Check if a token is expired. Returns the expiry if not expired
|
||||
func checkTokenExpired(expiry interface{}) (int64, error) {
|
||||
// Coerce the expiry to a float64 to avoid scientific notation
|
||||
expFloat, ok := expiry.(float64)
|
||||
if !ok {
|
||||
return 0, errors.New("Missing or invalid 'exp' claim")
|
||||
}
|
||||
// Convert to the int64 time we expect :)
|
||||
expiryTime := int64(expFloat)
|
||||
|
||||
// Check if its expired
|
||||
isExpired := time.Now().After(time.Unix(expiryTime, 0))
|
||||
if isExpired {
|
||||
return 0, errors.New("Token has expired")
|
||||
}
|
||||
return expiryTime, nil
|
||||
}
|
||||
|
||||
// Check if a token has a valid issuer. Returns the issuer if valid
|
||||
func checkTokenIssuer(trustedHost string, issuer interface{}) (string, error) {
|
||||
issuerVal, ok := issuer.(string)
|
||||
if !ok {
|
||||
return "", errors.New("Missing or invalid 'iss' claim")
|
||||
}
|
||||
if issuer != trustedHost {
|
||||
return "", errors.New("Issuer does not matched trusted host")
|
||||
}
|
||||
return issuerVal, nil
|
||||
}
|
||||
|
||||
// Check the scope matches the expected scope. Returns scope if true
|
||||
func getTokenScope(scope interface{}) (string, error) {
|
||||
scopeStr, ok := scope.(string)
|
||||
if !ok {
|
||||
return "", errors.New("Missing or invalid 'scope' claim")
|
||||
}
|
||||
return scopeStr, nil
|
||||
}
|
||||
|
||||
// Get the TTL of the token, either "session" or "exp"
|
||||
func getTokenTTL(ttl interface{}) (string, error) {
|
||||
ttlStr, ok := ttl.(string)
|
||||
if !ok {
|
||||
return "", errors.New("Missing or invalid 'ttl' claim")
|
||||
}
|
||||
if ttlStr != "exp" && ttlStr != "session" {
|
||||
return "", errors.New("TTL value is not recognised")
|
||||
}
|
||||
return ttlStr, nil
|
||||
}
|
||||
|
||||
// Get the time the token was issued at
|
||||
func getIssuedTime(issued interface{}) (int64, error) {
|
||||
// Same float64 -> int64 trick as expiry
|
||||
issuedFloat, ok := issued.(float64)
|
||||
if !ok {
|
||||
return 0, errors.New("Missing or invalid 'iat' claim")
|
||||
}
|
||||
issuedAt := int64(issuedFloat)
|
||||
return issuedAt, nil
|
||||
}
|
||||
|
||||
// Get the freshness expiry timestamp
|
||||
func getFreshTime(fresh interface{}) (int64, error) {
|
||||
freshUntil, ok := fresh.(float64)
|
||||
if !ok {
|
||||
return 0, errors.New("Missing or invalid 'fresh' claim")
|
||||
}
|
||||
return int64(freshUntil), nil
|
||||
}
|
||||
|
||||
// Get the subject of the token
|
||||
func getTokenSubject(sub interface{}) (int, error) {
|
||||
subject, ok := sub.(float64)
|
||||
if !ok {
|
||||
return 0, errors.New("Missing or invalid 'sub' claim")
|
||||
}
|
||||
return int(subject), nil
|
||||
}
|
||||
|
||||
// Get the JTI of the token
|
||||
func getTokenJTI(jti interface{}) (uuid.UUID, error) {
|
||||
jtiStr, ok := jti.(string)
|
||||
if !ok {
|
||||
return uuid.UUID{}, errors.New("Missing or invalid 'jti' claim")
|
||||
}
|
||||
jtiUUID, err := uuid.Parse(jtiStr)
|
||||
if err != nil {
|
||||
return uuid.UUID{}, errors.New("JTI is not a valid UUID")
|
||||
}
|
||||
return jtiUUID, nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Revoke a token by adding it to the database
|
||||
func RevokeToken(ctx context.Context, tx *db.SafeWTX, t Token) error {
|
||||
jti := t.GetJTI()
|
||||
exp := t.GetEXP()
|
||||
query := `INSERT INTO jwtblacklist (jti, exp) VALUES (?, ?)`
|
||||
_, err := tx.Exec(ctx, query, jti, exp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Exec")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if a token has been revoked. Returns true if not revoked.
|
||||
func CheckTokenNotRevoked(ctx context.Context, tx db.SafeTX, t Token) (bool, error) {
|
||||
jti := t.GetJTI()
|
||||
query := `SELECT 1 FROM jwtblacklist WHERE jti = ? LIMIT 1`
|
||||
rows, err := tx.Query(ctx, query, jti)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "tx.Query")
|
||||
}
|
||||
defer rows.Close()
|
||||
revoked := rows.Next()
|
||||
return !revoked, nil
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"projectreshoot/internal/models"
|
||||
"projectreshoot/pkg/db"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Token interface {
|
||||
GetJTI() uuid.UUID
|
||||
GetEXP() int64
|
||||
GetScope() string
|
||||
GetUser(ctx context.Context, tx db.SafeTX) (*models.User, error)
|
||||
}
|
||||
|
||||
// Access token
|
||||
type AccessToken struct {
|
||||
ISS string // Issuer, generally TrustedHost
|
||||
IAT int64 // Time issued at
|
||||
EXP int64 // Time expiring at
|
||||
TTL string // Time-to-live: "session" or "exp". Used with 'remember me'
|
||||
SUB int // Subject (user) ID
|
||||
JTI uuid.UUID // UUID-4 used for identifying blacklisted tokens
|
||||
Fresh int64 // Time freshness expiring at
|
||||
Scope string // Should be "access"
|
||||
}
|
||||
|
||||
// Refresh token
|
||||
type RefreshToken struct {
|
||||
ISS string // Issuer, generally TrustedHost
|
||||
IAT int64 // Time issued at
|
||||
EXP int64 // Time expiring at
|
||||
TTL string // Time-to-live: "session" or "exp". Used with 'remember me'
|
||||
SUB int // Subject (user) ID
|
||||
JTI uuid.UUID // UUID-4 used for identifying blacklisted tokens
|
||||
Scope string // Should be "refresh"
|
||||
}
|
||||
|
||||
func (a AccessToken) GetUser(ctx context.Context, tx db.SafeTX) (*models.User, error) {
|
||||
user, err := models.GetUserFromID(ctx, tx, a.SUB)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.GetUserFromID")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
func (r RefreshToken) GetUser(ctx context.Context, tx db.SafeTX) (*models.User, error) {
|
||||
user, err := models.GetUserFromID(ctx, tx, r.SUB)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "db.GetUserFromID")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (a AccessToken) GetJTI() uuid.UUID {
|
||||
return a.JTI
|
||||
}
|
||||
func (r RefreshToken) GetJTI() uuid.UUID {
|
||||
return r.JTI
|
||||
}
|
||||
func (a AccessToken) GetEXP() int64 {
|
||||
return a.EXP
|
||||
}
|
||||
func (r RefreshToken) GetEXP() int64 {
|
||||
return r.EXP
|
||||
}
|
||||
func (a AccessToken) GetScope() string {
|
||||
return a.Scope
|
||||
}
|
||||
func (r RefreshToken) GetScope() string {
|
||||
return r.Scope
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
// Takes a log level as string and converts it to a zerolog.Level interface.
|
||||
// If the string is not a valid input it will return zerolog.InfoLevel
|
||||
func GetLogLevel(level string) zerolog.Level {
|
||||
levels := map[string]zerolog.Level{
|
||||
"trace": zerolog.TraceLevel,
|
||||
"debug": zerolog.DebugLevel,
|
||||
"info": zerolog.InfoLevel,
|
||||
"warn": zerolog.WarnLevel,
|
||||
"error": zerolog.ErrorLevel,
|
||||
"fatal": zerolog.FatalLevel,
|
||||
"panic": zerolog.PanicLevel,
|
||||
}
|
||||
logLevel, valid := levels[level]
|
||||
if !valid {
|
||||
return zerolog.InfoLevel
|
||||
}
|
||||
return logLevel
|
||||
}
|
||||
|
||||
// Returns a pointer to a new log file with the specified path.
|
||||
// Remember to call file.Close() when finished writing to the log file
|
||||
func GetLogFile(path string) (*os.File, error) {
|
||||
logPath := filepath.Join(path, "server.log")
|
||||
file, err := os.OpenFile(
|
||||
logPath,
|
||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
|
||||
0663,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "os.OpenFile")
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// Get a pointer to a new zerolog.Logger with the specified level and output
|
||||
// Can provide a file, writer or both. Must provide at least one of the two
|
||||
func GetLogger(
|
||||
logLevel zerolog.Level,
|
||||
w io.Writer,
|
||||
logFile *os.File,
|
||||
logDir string,
|
||||
) (*zerolog.Logger, error) {
|
||||
if w == nil && logFile == nil {
|
||||
return nil, errors.New("No Writer provided for log output.")
|
||||
}
|
||||
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
var consoleWriter zerolog.ConsoleWriter
|
||||
if w != nil {
|
||||
consoleWriter = zerolog.ConsoleWriter{Out: w}
|
||||
}
|
||||
|
||||
var output io.Writer
|
||||
if logFile != nil {
|
||||
if w != nil {
|
||||
output = zerolog.MultiLevelWriter(logFile, consoleWriter)
|
||||
} else {
|
||||
output = logFile
|
||||
}
|
||||
} else {
|
||||
output = consoleWriter
|
||||
}
|
||||
logger := zerolog.New(output).
|
||||
With().
|
||||
Timestamp().
|
||||
Logger().
|
||||
Level(logLevel)
|
||||
|
||||
return &logger, nil
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"projectreshoot/pkg/config"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestConfig() (*config.Config, error) {
|
||||
os.Setenv("SECRET_KEY", ".")
|
||||
os.Setenv("TMDB_API_TOKEN", ".")
|
||||
cfg, err := config.GetConfig(map[string]string{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "config.GetConfig")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pressly/goose/v3"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func findMigrations() (*fs.FS, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "Makefile")); err == nil {
|
||||
migrationsdir := os.DirFS(filepath.Join(dir, "cmd", "migrate", "migrations"))
|
||||
return &migrationsdir, nil
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir { // Reached root
|
||||
return nil, errors.New("Unable to locate migrations directory")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func findTestData() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "Makefile")); err == nil {
|
||||
return filepath.Join(dir, "pkg", "tests", "testdata.sql"), nil
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir { // Reached root
|
||||
return "", errors.New("Unable to locate test data")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func migrateTestDB(wconn *sql.DB, version int64) error {
|
||||
migrations, err := findMigrations()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "findMigrations")
|
||||
}
|
||||
provider, err := goose.NewProvider(goose.DialectSQLite3, wconn, *migrations)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "goose.NewProvider")
|
||||
}
|
||||
ctx := context.Background()
|
||||
if _, err := provider.UpTo(ctx, version); err != nil {
|
||||
return errors.Wrap(err, "provider.UpTo")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadTestData(wconn *sql.DB) error {
|
||||
dataPath, err := findTestData()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "findSchema")
|
||||
}
|
||||
sqlBytes, err := os.ReadFile(dataPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "os.ReadFile")
|
||||
}
|
||||
dataSQL := string(sqlBytes)
|
||||
|
||||
_, err = wconn.Exec(dataSQL)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Exec")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns two db connection handles. First is a readwrite connection, second
|
||||
// is a read only connection
|
||||
func SetupTestDB(version int64) (*sql.DB, *sql.DB, error) {
|
||||
opts := "_journal_mode=WAL&_synchronous=NORMAL&_txlock=IMMEDIATE"
|
||||
file := fmt.Sprintf("file::memory:?cache=shared&%s", opts)
|
||||
wconn, err := sql.Open("sqlite", file)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "sql.Open")
|
||||
}
|
||||
|
||||
err = migrateTestDB(wconn, version)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "migrateTestDB")
|
||||
}
|
||||
err = loadTestData(wconn)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "loadTestData")
|
||||
}
|
||||
|
||||
opts = "_synchronous=NORMAL&mode=ro"
|
||||
file = fmt.Sprintf("file::memory:?cache=shared&%s", opts)
|
||||
rconn, err := sql.Open("sqlite", file)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "sql.Open")
|
||||
}
|
||||
return wconn, rconn, nil
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type TLogWriter struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface for TLogWriter.
|
||||
func (w *TLogWriter) Write(p []byte) (n int, err error) {
|
||||
w.t.Logf("%s", p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Return a fake logger to satisfy functions that expect one
|
||||
func NilLogger() *zerolog.Logger {
|
||||
logger := zerolog.New(nil)
|
||||
return &logger
|
||||
}
|
||||
|
||||
// Return a logger that makes use of the T.Log method to enable debugging tests
|
||||
func DebugLogger(t *testing.T) *zerolog.Logger {
|
||||
logger := zerolog.New(GetTLogWriter(t))
|
||||
return &logger
|
||||
}
|
||||
|
||||
func GetTLogWriter(t *testing.T) *TLogWriter {
|
||||
return &TLogWriter{t: t}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
INSERT INTO users VALUES(1,'testuser','hashedpassword',1738995274, 'bio');
|
||||
INSERT INTO jwtblacklist VALUES('0a6b338e-930a-43fe-8f70-1a6daed256fa', 33299675344);
|
||||
INSERT INTO jwtblacklist VALUES('b7fa51dc-8532-42e1-8756-5d25bfb2003a', 33299675344);
|
||||
@@ -1,32 +0,0 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Image Image `json:"images"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
SecureBaseURL string `json:"secure_base_url"`
|
||||
BackdropSizes []string `json:"backdrop_sizes"`
|
||||
LogoSizes []string `json:"logo_sizes"`
|
||||
PosterSizes []string `json:"poster_sizes"`
|
||||
ProfileSizes []string `json:"profile_sizes"`
|
||||
StillSizes []string `json:"still_sizes"`
|
||||
}
|
||||
|
||||
func GetConfig(token string) (*Config, error) {
|
||||
url := "https://api.themoviedb.org/3/configuration"
|
||||
data, err := tmdbGet(url, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdbGet")
|
||||
}
|
||||
config := Config{}
|
||||
json.Unmarshal(data, &config)
|
||||
return &config, nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Credits struct {
|
||||
ID int32 `json:"id"`
|
||||
Cast []Cast `json:"cast"`
|
||||
Crew []Crew `json:"crew"`
|
||||
}
|
||||
|
||||
type Cast struct {
|
||||
Adult bool `json:"adult"`
|
||||
Gender int `json:"gender"`
|
||||
ID int32 `json:"id"`
|
||||
KnownFor string `json:"known_for_department"`
|
||||
Name string `json:"name"`
|
||||
OriginalName string `json:"original_name"`
|
||||
Popularity int `json:"popularity"`
|
||||
Profile string `json:"profile_path"`
|
||||
CastID int32 `json:"cast_id"`
|
||||
Character string `json:"character"`
|
||||
CreditID string `json:"credit_id"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
type Crew struct {
|
||||
Adult bool `json:"adult"`
|
||||
Gender int `json:"gender"`
|
||||
ID int32 `json:"id"`
|
||||
KnownFor string `json:"known_for_department"`
|
||||
Name string `json:"name"`
|
||||
OriginalName string `json:"original_name"`
|
||||
Popularity int `json:"popularity"`
|
||||
Profile string `json:"profile_path"`
|
||||
CreditID string `json:"credit_id"`
|
||||
Department string `json:"department"`
|
||||
Job string `json:"job"`
|
||||
}
|
||||
|
||||
func GetCredits(movieid int32, token string) (*Credits, error) {
|
||||
url := fmt.Sprintf("https://api.themoviedb.org/3/movie/%v/credits?language=en-US", movieid)
|
||||
data, err := tmdbGet(url, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdbGet")
|
||||
}
|
||||
credits := Credits{}
|
||||
json.Unmarshal(data, &credits)
|
||||
return &credits, nil
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package tmdb
|
||||
|
||||
import "sort"
|
||||
|
||||
type BilledCrew struct {
|
||||
Name string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
func (credits *Credits) BilledCrew() []BilledCrew {
|
||||
crewmap := make(map[string][]string)
|
||||
billedcrew := []BilledCrew{}
|
||||
for _, crew := range credits.Crew {
|
||||
if crew.Job == "Director" ||
|
||||
crew.Job == "Screenplay" ||
|
||||
crew.Job == "Writer" ||
|
||||
crew.Job == "Novel" ||
|
||||
crew.Job == "Story" {
|
||||
crewmap[crew.Name] = append(crewmap[crew.Name], crew.Job)
|
||||
}
|
||||
}
|
||||
|
||||
for name, jobs := range crewmap {
|
||||
billedcrew = append(billedcrew, BilledCrew{Name: name, Roles: jobs})
|
||||
}
|
||||
for i := range billedcrew {
|
||||
sort.Strings(billedcrew[i].Roles)
|
||||
}
|
||||
sort.Slice(billedcrew, func(i, j int) bool {
|
||||
return billedcrew[i].Roles[0] < billedcrew[j].Roles[0]
|
||||
})
|
||||
return billedcrew
|
||||
}
|
||||
|
||||
func (billedcrew *BilledCrew) FRoles() string {
|
||||
jobs := ""
|
||||
for _, job := range billedcrew.Roles {
|
||||
jobs += job + ", "
|
||||
}
|
||||
return jobs[:len(jobs)-2]
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Movie struct {
|
||||
Adult bool `json:"adult"`
|
||||
Backdrop string `json:"backdrop_path"`
|
||||
Collection string `json:"belongs_to_collection"`
|
||||
Budget int `json:"budget"`
|
||||
Genres []Genre `json:"genres"`
|
||||
Homepage string `json:"homepage"`
|
||||
ID int32 `json:"id"`
|
||||
IMDbID string `json:"imdb_id"`
|
||||
OriginalLanguage string `json:"original_language"`
|
||||
OriginalTitle string `json:"original_title"`
|
||||
Overview string `json:"overview"`
|
||||
Popularity float32 `json:"popularity"`
|
||||
Poster string `json:"poster_path"`
|
||||
ProductionCompanies []ProductionCompany `json:"production_companies"`
|
||||
ProductionCountries []ProductionCountry `json:"production_countries"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Revenue int `json:"revenue"`
|
||||
Runtime int `json:"runtime"`
|
||||
SpokenLanguages []SpokenLanguage `json:"spoken_languages"`
|
||||
Status string `json:"status"`
|
||||
Tagline string `json:"tagline"`
|
||||
Title string `json:"title"`
|
||||
Video bool `json:"video"`
|
||||
}
|
||||
|
||||
func GetMovie(id int32, token string) (*Movie, error) {
|
||||
url := fmt.Sprintf("https://api.themoviedb.org/3/movie/%v?language=en-US", id)
|
||||
data, err := tmdbGet(url, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdbGet")
|
||||
}
|
||||
movie := Movie{}
|
||||
json.Unmarshal(data, &movie)
|
||||
return &movie, nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
func (movie *Movie) FRuntime() string {
|
||||
hours := movie.Runtime / 60
|
||||
mins := movie.Runtime % 60
|
||||
return fmt.Sprintf("%dh %02dm", hours, mins)
|
||||
}
|
||||
|
||||
func (movie *Movie) GetPoster(image *Image, size string) string {
|
||||
base, err := url.Parse(image.SecureBaseURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
fullPath := path.Join(base.Path, size, movie.Poster)
|
||||
base.Path = fullPath
|
||||
return base.String()
|
||||
}
|
||||
|
||||
func (movie *Movie) ReleaseYear() string {
|
||||
if movie.ReleaseDate == "" {
|
||||
return ""
|
||||
} else {
|
||||
return "(" + movie.ReleaseDate[:4] + ")"
|
||||
}
|
||||
}
|
||||
|
||||
func (movie *Movie) FGenres() string {
|
||||
genres := ""
|
||||
for _, genre := range movie.Genres {
|
||||
genres += genre.Name + ", "
|
||||
}
|
||||
if len(genres) > 2 {
|
||||
return genres[:len(genres)-2]
|
||||
}
|
||||
return genres
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func tmdbGet(url string, token string) ([]byte, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "http.NewRequest")
|
||||
}
|
||||
req.Header.Add("accept", "application/json")
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "http.DefaultClient.Do")
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "io.ReadAll")
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Page int `json:"page"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalResults int `json:"total_results"`
|
||||
}
|
||||
|
||||
type ResultMovies struct {
|
||||
Result
|
||||
Results []ResultMovie `json:"results"`
|
||||
}
|
||||
type ResultMovie struct {
|
||||
Adult bool `json:"adult"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
GenreIDs []int `json:"genre_ids"`
|
||||
ID int32 `json:"id"`
|
||||
OriginalLanguage string `json:"original_language"`
|
||||
OriginalTitle string `json:"original_title"`
|
||||
Overview string `json:"overview"`
|
||||
Popularity int `json:"popularity"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Title string `json:"title"`
|
||||
Video bool `json:"video"`
|
||||
VoteAverage int `json:"vote_average"`
|
||||
VoteCount int `json:"vote_count"`
|
||||
}
|
||||
|
||||
func (movie *ResultMovie) GetPoster(image *Image, size string) string {
|
||||
base, err := url.Parse(image.SecureBaseURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
fullPath := path.Join(base.Path, size, movie.PosterPath)
|
||||
base.Path = fullPath
|
||||
return base.String()
|
||||
}
|
||||
|
||||
func (movie *ResultMovie) ReleaseYear() string {
|
||||
if movie.ReleaseDate == "" {
|
||||
return ""
|
||||
} else {
|
||||
return "(" + movie.ReleaseDate[:4] + ")"
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: genres list https://developer.themoviedb.org/reference/genre-movie-list
|
||||
// func (movie *ResultMovie) FGenres() string {
|
||||
// genres := ""
|
||||
// for _, genre := range movie.Genres {
|
||||
// genres += genre.Name + ", "
|
||||
// }
|
||||
// return genres[:len(genres)-2]
|
||||
// }
|
||||
|
||||
func SearchMovies(token string, query string, adult bool, page int) (*ResultMovies, error) {
|
||||
url := "https://api.themoviedb.org/3/search/movie" +
|
||||
fmt.Sprintf("?query=%s", url.QueryEscape(query)) +
|
||||
fmt.Sprintf("&include_adult=%t", adult) +
|
||||
fmt.Sprintf("&page=%v", page) +
|
||||
"&language=en-US"
|
||||
response, err := tmdbGet(url, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdbGet")
|
||||
}
|
||||
var results ResultMovies
|
||||
json.Unmarshal(response, &results)
|
||||
return &results, nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package tmdb
|
||||
|
||||
type Genre struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ProductionCompany struct {
|
||||
ID int `json:"id"`
|
||||
Logo string `json:"logo_path"`
|
||||
Name string `json:"name"`
|
||||
OriginCountry string `json:"origin_country"`
|
||||
}
|
||||
|
||||
type ProductionCountry struct {
|
||||
ISO_3166_1 string `json:"iso_3166_1"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SpokenLanguage struct {
|
||||
EnglishName string `json:"english_name"`
|
||||
ISO_639_1 string `json:"iso_639_1"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
Reference in New Issue
Block a user