From 1d9af44d0a7999b5bed3cd85489166dfe02b2326 Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Wed, 5 Mar 2025 20:18:28 +1100 Subject: [PATCH] refactor: changed file structure --- .air.toml | 2 +- .github/workflows/deploy_production.yaml | 4 +- .github/workflows/deploy_staging.yaml | 4 +- .gitignore | 6 +- Makefile | 20 +- {migrate => cmd/migrate}/migrate.go | 2 +- .../migrate}/migrations/00001_init.sql | 0 cmd/projectreshoot/dbconn.go | 37 + cmd/projectreshoot/flags.go | 30 + cmd/projectreshoot/main.go | 16 + .../projectreshoot/main_test.go | 0 cmd/projectreshoot/run.go | 119 + cmd/projectreshoot/signals.go | 50 + cmd/projectreshoot/tester.go | 32 + db/safetx.go | 61 - deploy/db/migrate.sh | 2 +- go.mod | 1 + go.sum | 2 + {handler => internal/handler}/account.go | 17 +- {handler => internal/handler}/errorpage.go | 2 +- {handler => internal/handler}/index.go | 2 +- {handler => internal/handler}/login.go | 17 +- {handler => internal/handler}/logout.go | 14 +- {handler => internal/handler}/movie.go | 6 +- {handler => internal/handler}/movie_search.go | 8 +- {handler => internal/handler}/page.go | 0 {handler => internal/handler}/profile.go | 2 +- .../handler}/reauthenticatate.go | 18 +- {handler => internal/handler}/register.go | 19 +- {handler => internal/handler}/static.go | 0 {server => internal/httpserver}/routes.go | 12 +- {server => internal/httpserver}/server.go | 32 +- .../middleware}/authentication.go | 19 +- .../middleware}/authentication_test.go | 10 +- {middleware => internal/middleware}/gzip.go | 0 .../middleware}/logging.go | 4 +- .../middleware}/pageprotection.go | 4 +- .../middleware}/pageprotection_test.go | 8 +- .../middleware}/reauthentication.go | 2 +- .../middleware}/reauthentication_test.go | 8 +- {middleware => internal/middleware}/start.go | 2 +- {db => internal/models}/user.go | 9 +- {db => internal/models}/user_functions.go | 13 +- .../view}/component/account/changebio.templ | 2 +- .../view/component/account/changebio_templ.go | 60 + .../component/account/changepassword.templ | 0 .../component/account/changepassword_templ.go | 55 + .../component/account/changeusername.templ | 2 +- .../component/account/changeusername_templ.go | 62 + .../view}/component/account/container.templ | 0 .../view/component/account/container_templ.go | 77 + .../view}/component/account/general.templ | 0 .../view/component/account/general_templ.go | 52 + .../view}/component/account/security.templ | 0 .../view/component/account/security_templ.go | 48 + .../view}/component/account/selectmenu.templ | 0 .../component/account/selectmenu_templ.go | 131 ++ .../view}/component/footer/footer.templ | 0 .../view/component/footer/footer_templ.go | 92 + .../view}/component/form/confirmpass.templ | 0 .../view/component/form/confirmpass_templ.go | 55 + .../view}/component/form/loginform.templ | 0 .../view/component/form/loginform_templ.go | 60 + .../view}/component/form/registerform.templ | 0 .../view/component/form/registerform_templ.go | 63 + .../view}/component/nav/navbar.templ | 0 internal/view/component/nav/navbar_templ.go | 77 + .../view}/component/nav/navbarleft.templ | 0 .../view/component/nav/navbarleft_templ.go | 73 + .../view}/component/nav/navbarright.templ | 2 +- .../view/component/nav/navbarright_templ.go | 124 ++ .../view}/component/nav/sidenav.templ | 2 +- internal/view/component/nav/sidenav_templ.go | 86 + .../popup/confirmPasswordModal.templ | 2 +- .../popup/confirmPasswordModal_templ.go | 51 + .../view}/component/popup/error500Popup.templ | 0 .../component/popup/error500Popup_templ.go | 40 + .../view}/component/popup/error503Popup.templ | 0 .../component/popup/error503Popup_templ.go | 40 + .../component/search/movies_results.templ | 2 +- .../component/search/movies_results_templ.go | 132 ++ {view => internal/view}/layout/global.templ | 6 +- internal/view/layout/global_templ.go | 99 + {view => internal/view}/page/about.templ | 2 +- internal/view/page/about_templ.go | 61 + {view => internal/view}/page/account.templ | 4 +- internal/view/page/account_templ.go | 61 + {view => internal/view}/page/error.templ | 2 +- internal/view/page/error_templ.go | 103 + {view => internal/view}/page/index.templ | 2 +- internal/view/page/index_templ.go | 61 + {view => internal/view}/page/login.templ | 4 +- internal/view/page/login_templ.go | 70 + {view => internal/view}/page/movie.templ | 4 +- .../view}/page/movie_search.templ | 2 +- internal/view/page/movie_search_templ.go | 60 + internal/view/page/movie_templ.go | 188 ++ {view => internal/view}/page/profile.templ | 4 +- internal/view/page/profile_templ.go | 75 + {view => internal/view}/page/register.templ | 4 +- internal/view/page/register_templ.go | 70 + main.go | 233 -- {config => pkg/config}/config.go | 4 +- {config => pkg/config}/environment.go | 0 {contexts => pkg/contexts}/keys.go | 0 {contexts => pkg/contexts}/request_timer.go | 0 {contexts => pkg/contexts}/user.go | 4 +- {cookies => pkg/cookies}/functions.go | 0 {cookies => pkg/cookies}/pagefrom.go | 0 {cookies => pkg/cookies}/tokens.go | 8 +- {db => pkg/db}/connection.go | 22 +- {db => pkg/db}/safeconn.go | 47 +- {db => pkg/db}/safeconntx_test.go | 18 +- pkg/db/safetx.go | 163 ++ pkg/embedfs/embedfs.go | 20 + pkg/embedfs/files/assets/error.png | Bin 0 -> 21893 bytes {static => pkg/embedfs/files}/css/input.css | 0 pkg/embedfs/files/css/output.css | 1913 +++++++++++++++++ {static => pkg/embedfs/files}/favicon.ico | Bin {jwt => pkg/jwt}/create.go | 8 +- {jwt => pkg/jwt}/parse.go | 8 +- {jwt => pkg/jwt}/revoke.go | 6 +- {jwt => pkg/jwt}/tokens.go | 13 +- {logging => pkg/logging}/logger.go | 0 {tests => pkg/tests}/config.go | 2 +- pkg/tests/database.go | 116 + {tests => pkg/tests}/logger.go | 0 {tests => pkg/tests}/testdata.sql | 0 {tmdb => pkg/tmdb}/config.go | 0 {tmdb => pkg/tmdb}/credits.go | 0 {tmdb => pkg/tmdb}/crew_functions.go | 0 {tmdb => pkg/tmdb}/movie.go | 0 {tmdb => pkg/tmdb}/movie_functions.go | 0 {tmdb => pkg/tmdb}/request.go | 0 {tmdb => pkg/tmdb}/search.go | 0 {tmdb => pkg/tmdb}/structs.go | 4 - tests/database.go | 90 - 137 files changed, 4986 insertions(+), 581 deletions(-) rename {migrate => cmd/migrate}/migrate.go (95%) rename {migrate => cmd/migrate}/migrations/00001_init.sql (100%) create mode 100644 cmd/projectreshoot/dbconn.go create mode 100644 cmd/projectreshoot/flags.go create mode 100644 cmd/projectreshoot/main.go rename main_test.go => cmd/projectreshoot/main_test.go (100%) create mode 100644 cmd/projectreshoot/run.go create mode 100644 cmd/projectreshoot/signals.go create mode 100644 cmd/projectreshoot/tester.go delete mode 100644 db/safetx.go rename {handler => internal/handler}/account.go (93%) rename {handler => internal/handler}/errorpage.go (94%) rename {handler => internal/handler}/index.go (91%) rename {handler => internal/handler}/login.go (89%) rename {handler => internal/handler}/logout.go (94%) rename {handler => internal/handler}/movie.go (92%) rename {handler => internal/handler}/movie_search.go (84%) rename {handler => internal/handler}/page.go (100%) rename {handler => internal/handler}/profile.go (84%) rename {handler => internal/handler}/reauthenticatate.go (92%) rename {handler => internal/handler}/register.go (86%) rename {handler => internal/handler}/static.go (100%) rename {server => internal/httpserver}/routes.go (91%) rename {server => internal/httpserver}/server.go (55%) rename {middleware => internal/middleware}/authentication.go (92%) rename {middleware => internal/middleware}/authentication_test.go (97%) rename {middleware => internal/middleware}/gzip.go (100%) rename {middleware => internal/middleware}/logging.go (94%) rename {middleware => internal/middleware}/pageprotection.go (92%) rename {middleware => internal/middleware}/pageprotection_test.go (93%) rename {middleware => internal/middleware}/reauthentication.go (91%) rename {middleware => internal/middleware}/reauthentication_test.go (93%) rename {middleware => internal/middleware}/start.go (91%) rename {db => internal/models}/user.go (83%) rename {db => internal/models}/user_functions.go (88%) rename {view => internal/view}/component/account/changebio.templ (98%) create mode 100644 internal/view/component/account/changebio_templ.go rename {view => internal/view}/component/account/changepassword.templ (100%) create mode 100644 internal/view/component/account/changepassword_templ.go rename {view => internal/view}/component/account/changeusername.templ (98%) create mode 100644 internal/view/component/account/changeusername_templ.go rename {view => internal/view}/component/account/container.templ (100%) create mode 100644 internal/view/component/account/container_templ.go rename {view => internal/view}/component/account/general.templ (100%) create mode 100644 internal/view/component/account/general_templ.go rename {view => internal/view}/component/account/security.templ (100%) create mode 100644 internal/view/component/account/security_templ.go rename {view => internal/view}/component/account/selectmenu.templ (100%) create mode 100644 internal/view/component/account/selectmenu_templ.go rename {view => internal/view}/component/footer/footer.templ (100%) create mode 100644 internal/view/component/footer/footer_templ.go rename {view => internal/view}/component/form/confirmpass.templ (100%) create mode 100644 internal/view/component/form/confirmpass_templ.go rename {view => internal/view}/component/form/loginform.templ (100%) create mode 100644 internal/view/component/form/loginform_templ.go rename {view => internal/view}/component/form/registerform.templ (100%) create mode 100644 internal/view/component/form/registerform_templ.go rename {view => internal/view}/component/nav/navbar.templ (100%) create mode 100644 internal/view/component/nav/navbar_templ.go rename {view => internal/view}/component/nav/navbarleft.templ (100%) create mode 100644 internal/view/component/nav/navbarleft_templ.go rename {view => internal/view}/component/nav/navbarright.templ (98%) create mode 100644 internal/view/component/nav/navbarright_templ.go rename {view => internal/view}/component/nav/sidenav.templ (97%) create mode 100644 internal/view/component/nav/sidenav_templ.go rename {view => internal/view}/component/popup/confirmPasswordModal.templ (88%) create mode 100644 internal/view/component/popup/confirmPasswordModal_templ.go rename {view => internal/view}/component/popup/error500Popup.templ (100%) create mode 100644 internal/view/component/popup/error500Popup_templ.go rename {view => internal/view}/component/popup/error503Popup.templ (100%) create mode 100644 internal/view/component/popup/error503Popup_templ.go rename {view => internal/view}/component/search/movies_results.templ (97%) create mode 100644 internal/view/component/search/movies_results_templ.go rename {view => internal/view}/layout/global.templ (96%) create mode 100644 internal/view/layout/global_templ.go rename {view => internal/view}/page/about.templ (97%) create mode 100644 internal/view/page/about_templ.go rename {view => internal/view}/page/account.templ (56%) create mode 100644 internal/view/page/account_templ.go rename {view => internal/view}/page/error.templ (95%) create mode 100644 internal/view/page/error_templ.go rename {view => internal/view}/page/index.templ (85%) create mode 100644 internal/view/page/index_templ.go rename {view => internal/view}/page/login.templ (91%) create mode 100644 internal/view/page/login_templ.go rename {view => internal/view}/page/movie.templ (97%) rename {view => internal/view}/page/movie_search.templ (94%) create mode 100644 internal/view/page/movie_search_templ.go create mode 100644 internal/view/page/movie_templ.go rename {view => internal/view}/page/profile.templ (68%) create mode 100644 internal/view/page/profile_templ.go rename {view => internal/view}/page/register.templ (91%) create mode 100644 internal/view/page/register_templ.go delete mode 100644 main.go rename {config => pkg/config}/config.go (98%) rename {config => pkg/config}/environment.go (100%) rename {contexts => pkg/contexts}/keys.go (100%) rename {contexts => pkg/contexts}/request_timer.go (100%) rename {contexts => pkg/contexts}/user.go (91%) rename {cookies => pkg/cookies}/functions.go (100%) rename {cookies => pkg/cookies}/pagefrom.go (100%) rename {cookies => pkg/cookies}/tokens.go (93%) rename {db => pkg/db}/connection.go (65%) rename {db => pkg/db}/safeconn.go (73%) rename {db => pkg/db}/safeconntx_test.go (90%) create mode 100644 pkg/db/safetx.go create mode 100644 pkg/embedfs/embedfs.go create mode 100644 pkg/embedfs/files/assets/error.png rename {static => pkg/embedfs/files}/css/input.css (100%) create mode 100644 pkg/embedfs/files/css/output.css rename {static => pkg/embedfs/files}/favicon.ico (100%) rename {jwt => pkg/jwt}/create.go (94%) rename {jwt => pkg/jwt}/parse.go (98%) rename {jwt => pkg/jwt}/revoke.go (77%) rename {jwt => pkg/jwt}/tokens.go (79%) rename {logging => pkg/logging}/logger.go (100%) rename {tests => pkg/tests}/config.go (91%) create mode 100644 pkg/tests/database.go rename {tests => pkg/tests}/logger.go (100%) rename {tests => pkg/tests}/testdata.sql (100%) rename {tmdb => pkg/tmdb}/config.go (100%) rename {tmdb => pkg/tmdb}/credits.go (100%) rename {tmdb => pkg/tmdb}/crew_functions.go (100%) rename {tmdb => pkg/tmdb}/movie.go (100%) rename {tmdb => pkg/tmdb}/movie_functions.go (100%) rename {tmdb => pkg/tmdb}/request.go (100%) rename {tmdb => pkg/tmdb}/search.go (100%) rename {tmdb => pkg/tmdb}/structs.go (94%) delete mode 100644 tests/database.go diff --git a/.air.toml b/.air.toml index d1066a7..921489b 100644 --- a/.air.toml +++ b/.air.toml @@ -5,7 +5,7 @@ tmp_dir = "tmp" [build] args_bin = [] bin = "./tmp/main" - cmd = "go build -o ./tmp/main ." + cmd = "go build -o ./tmp/main ./cmd/reshoot" delay = 1000 exclude_dir = [] exclude_file = [] diff --git a/.github/workflows/deploy_production.yaml b/.github/workflows/deploy_production.yaml index 058d43d..9166609 100644 --- a/.github/workflows/deploy_production.yaml +++ b/.github/workflows/deploy_production.yaml @@ -53,10 +53,10 @@ jobs: echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config ssh -i ~/.ssh/id_ed25519 $USER@$HOST mkdir -p $DIR - scp -i ~/.ssh/id_ed25519 projectreshoot-production-${GITHUB_SHA} $USER@$HOST:$DIR + scp -i ~/.ssh/id_ed25519 ./bin/projectreshoot-production-${GITHUB_SHA} $USER@$HOST:$DIR ssh -i ~/.ssh/id_ed25519 $USER@$HOST mkdir -p $MIG_DIR - scp -i ~/.ssh/id_ed25519 prmigrate-production-${GITHUB_SHA} $USER@$HOST:$MIG_DIR + scp -i ~/.ssh/id_ed25519 .bin/migrate-production-${GITHUB_SHA} $USER@$HOST:$MIG_DIR scp -i ~/.ssh/id_ed25519 ./deploy/db/backup.sh $USER@$HOST:$MIG_DIR scp -i ~/.ssh/id_ed25519 ./deploy/db/migrate.sh $USER@$HOST:$MIG_DIR diff --git a/.github/workflows/deploy_staging.yaml b/.github/workflows/deploy_staging.yaml index 539097e..07230d6 100644 --- a/.github/workflows/deploy_staging.yaml +++ b/.github/workflows/deploy_staging.yaml @@ -53,10 +53,10 @@ jobs: echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config ssh -i ~/.ssh/id_ed25519 $USER@$HOST mkdir -p $DIR - scp -i ~/.ssh/id_ed25519 projectreshoot-staging-${GITHUB_SHA} $USER@$HOST:$DIR + scp -i ~/.ssh/id_ed25519 ./bin/projectreshoot-staging-${GITHUB_SHA} $USER@$HOST:$DIR ssh -i ~/.ssh/id_ed25519 $USER@$HOST mkdir -p $MIG_DIR - scp -i ~/.ssh/id_ed25519 prmigrate-staging-${GITHUB_SHA} $USER@$HOST:$MIG_DIR + scp -i ~/.ssh/id_ed25519 ./bin/migrate-staging-${GITHUB_SHA} $USER@$HOST:$MIG_DIR scp -i ~/.ssh/id_ed25519 ./deploy/db/backup.sh $USER@$HOST:$MIG_DIR scp -i ~/.ssh/id_ed25519 ./deploy/db/migrate.sh $USER@$HOST:$MIG_DIR diff --git a/.gitignore b/.gitignore index 9e3f92b..9d8ebf9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,9 @@ .env -query.sql -*.db +*.db* .logs/ server.log +bin/ tmp/ -prmigrate -projectreshoot static/css/output.css view/**/*_templ.go view/**/*_templ.txt diff --git a/Makefile b/Makefile index acabebf..78aa20a 100644 --- a/Makefile +++ b/Makefile @@ -5,16 +5,16 @@ BINARY_NAME=projectreshoot build: - tailwindcss -i ./static/css/input.css -o ./static/css/output.css && \ + tailwindcss -i ./pkg/embedfs/files/css/input.css -o ./pkg/embedfs/files/css/output.css && \ go mod tidy && \ templ generate && \ - go generate && \ - go build -ldflags="-w -s" -o ${BINARY_NAME}${SUFFIX} + go generate ./cmd/projectreshoot && \ + go build -ldflags="-w -s" -o ./bin/${BINARY_NAME}${SUFFIX} ./cmd/projectreshoot dev: templ generate --watch &\ air &\ - tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watch + tailwindcss -i ./pkg/embedfs/files/css/input.css -o ./pkg/embedfs/files/css/output.css --watch tester: go mod tidy && \ @@ -23,15 +23,15 @@ tester: test: go mod tidy && \ templ generate && \ - go generate && \ - go test . - go test ./db - go test ./middleware + go generate ./cmd/projectreshoot && \ + go test ./cmd/projectreshoot + go test ./pkg/db + go test ./internal/middleware clean: go clean migrate: go mod tidy && \ - go generate && \ - go build -ldflags="-w -s" -o prmigrate${SUFFIX} ./migrate + go generate ./cmd/migrate && \ + go build -ldflags="-w -s" -o ./bin/migrate${SUFFIX} ./cmd/migrate diff --git a/migrate/migrate.go b/cmd/migrate/migrate.go similarity index 95% rename from migrate/migrate.go rename to cmd/migrate/migrate.go index dd8a99e..da64943 100644 --- a/migrate/migrate.go +++ b/cmd/migrate/migrate.go @@ -20,7 +20,7 @@ var migrationsFS embed.FS func main() { if len(os.Args) != 4 { - fmt.Println("Usage: prmigrate up-to|down-to ") + fmt.Println("Usage: migrate up-to|down-to ") os.Exit(1) } diff --git a/migrate/migrations/00001_init.sql b/cmd/migrate/migrations/00001_init.sql similarity index 100% rename from migrate/migrations/00001_init.sql rename to cmd/migrate/migrations/00001_init.sql diff --git a/cmd/projectreshoot/dbconn.go b/cmd/projectreshoot/dbconn.go new file mode 100644 index 0000000..650f8c1 --- /dev/null +++ b/cmd/projectreshoot/dbconn.go @@ -0,0 +1,37 @@ +package main + +import ( + "projectreshoot/pkg/config" + "projectreshoot/pkg/db" + "projectreshoot/pkg/tests" + "strconv" + + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +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 + } +} diff --git a/cmd/projectreshoot/flags.go b/cmd/projectreshoot/flags.go new file mode 100644 index 0000000..3501397 --- /dev/null +++ b/cmd/projectreshoot/flags.go @@ -0,0 +1,30 @@ +package main + +import ( + "flag" + "strconv" +) + +func setupFlags() map[string]string { + // Parse commandline args + host := flag.String("host", "", "Override host to listen on") + port := flag.String("port", "", "Override port to listen on") + test := flag.Bool("test", false, "Run server in test mode") + tester := flag.Bool("tester", false, "Run tester function instead of main program") + dbver := flag.Bool("dbver", false, "Get the version of the database required") + loglevel := flag.String("loglevel", "", "Set log level") + logoutput := flag.String("logoutput", "", "Set log destination (file, console or both)") + flag.Parse() + + // Map the args for easy access + args := map[string]string{ + "host": *host, + "port": *port, + "test": strconv.FormatBool(*test), + "tester": strconv.FormatBool(*tester), + "dbver": strconv.FormatBool(*dbver), + "loglevel": *loglevel, + "logoutput": *logoutput, + } + return args +} diff --git a/cmd/projectreshoot/main.go b/cmd/projectreshoot/main.go new file mode 100644 index 0000000..65f289b --- /dev/null +++ b/cmd/projectreshoot/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "context" + "fmt" + "os" +) + +func main() { + args := setupFlags() + ctx := context.Background() + if err := run(ctx, os.Stdout, args); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } +} diff --git a/main_test.go b/cmd/projectreshoot/main_test.go similarity index 100% rename from main_test.go rename to cmd/projectreshoot/main_test.go diff --git a/cmd/projectreshoot/run.go b/cmd/projectreshoot/run.go new file mode 100644 index 0000000..c151eaf --- /dev/null +++ b/cmd/projectreshoot/run.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "projectreshoot/internal/httpserver" + "projectreshoot/pkg/config" + "projectreshoot/pkg/embedfs" + "projectreshoot/pkg/logging" + "sync" + "time" + + "github.com/pkg/errors" +) + +var maint uint32 // atomic: 1 if in maintenance mode + +// Initializes and runs the server +func run(ctx context.Context, w io.Writer, args map[string]string) error { + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) + defer cancel() + + config, err := config.GetConfig(args) + if err != nil { + return errors.Wrap(err, "server.GetConfig") + } + + // Return the version of the database required + if args["dbver"] == "true" { + fmt.Fprintf(w, "Database version: %s\n", config.DBName) + return nil + } + + // Setup the logfile + var logfile *os.File = nil + if config.LogOutput == "both" || config.LogOutput == "file" { + logfile, err = logging.GetLogFile(config.LogDir) + if err != nil { + return errors.Wrap(err, "logging.GetLogFile") + } + defer logfile.Close() + } + + // Setup the console writer + var consoleWriter io.Writer + if config.LogOutput == "both" || config.LogOutput == "console" { + consoleWriter = w + } + + // Setup the logger + logger, err := logging.GetLogger( + config.LogLevel, + consoleWriter, + logfile, + config.LogDir, + ) + if err != nil { + return errors.Wrap(err, "logging.GetLogger") + } + + // Setup the database connection + logger.Debug().Msg("Config loaded and logger started") + logger.Debug().Msg("Connecting to database") + conn, err := setupDBConn(args, logger, config) + if err != nil { + return errors.Wrap(err, "setupDBConn") + } + defer conn.Close() + + // Setup embedded files + logger.Debug().Msg("Getting embedded files") + staticFS, err := embedfs.GetEmbeddedFS() + if err != nil { + return errors.Wrap(err, "getStaticFiles") + } + + logger.Debug().Msg("Setting up HTTP server") + httpServer := httpserver.NewServer(config, logger, conn, &staticFS, &maint) + + // 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 + } + + // Setups a channel to listen for os.Signal + handleMaintSignals(conn, config, httpServer, logger) + + // Runs the http server + logger.Debug().Msg("Starting up the HTTP server") + go func() { + logger.Info().Str("address", httpServer.Addr).Msg("Listening for requests") + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Error().Err(err).Msg("Error listening and serving") + } + }() + + // Handles graceful shutdown + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + <-ctx.Done() + shutdownCtx := context.Background() + shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second) + defer cancel() + 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 +} diff --git a/cmd/projectreshoot/signals.go b/cmd/projectreshoot/signals.go new file mode 100644 index 0000000..e608cc3 --- /dev/null +++ b/cmd/projectreshoot/signals.go @@ -0,0 +1,50 @@ +package main + +import ( + "net/http" + "os" + "os/signal" + "projectreshoot/pkg/config" + "projectreshoot/pkg/db" + "sync/atomic" + "syscall" + "time" + + "github.com/rs/zerolog" +) + +// Handle SIGUSR1 and SIGUSR2 syscalls to toggle maintenance mode +func handleMaintSignals( + conn *db.SafeConn, + config *config.Config, + srv *http.Server, + logger *zerolog.Logger, +) { + logger.Debug().Msg("Starting signal listener") + ch := make(chan os.Signal, 1) + srv.RegisterOnShutdown(func() { + logger.Debug().Msg("Shutting down signal listener") + close(ch) + }) + go func() { + for sig := range ch { + switch sig { + case syscall.SIGUSR1: + 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) + } + } + } + }() + signal.Notify(ch, syscall.SIGUSR1, syscall.SIGUSR2) +} diff --git a/cmd/projectreshoot/tester.go b/cmd/projectreshoot/tester.go new file mode 100644 index 0000000..3932bf7 --- /dev/null +++ b/cmd/projectreshoot/tester.go @@ -0,0 +1,32 @@ +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") +} diff --git a/db/safetx.go b/db/safetx.go deleted file mode 100644 index 5a3f06e..0000000 --- a/db/safetx.go +++ /dev/null @@ -1,61 +0,0 @@ -package db - -import ( - "context" - "database/sql" - - "github.com/pkg/errors" -) - -// Extends sql.Tx for use with SafeConn -type SafeTX struct { - tx *sql.Tx - sc *SafeConn -} - -// Query the database inside the transaction -func (stx *SafeTX) Query( - ctx context.Context, - query string, - args ...interface{}, -) (*sql.Rows, error) { - if stx.tx == nil { - return nil, errors.New("Cannot query without a transaction") - } - return stx.tx.QueryContext(ctx, query, args...) -} - -// Exec a statement on the database inside the transaction -func (stx *SafeTX) Exec( - ctx context.Context, - query string, - args ...interface{}, -) (sql.Result, error) { - if stx.tx == nil { - return nil, errors.New("Cannot exec without a transaction") - } - return stx.tx.ExecContext(ctx, query, args...) -} - -// Commit the current transaction and release the read lock -func (stx *SafeTX) 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 *SafeTX) 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 -} diff --git a/deploy/db/migrate.sh b/deploy/db/migrate.sh index dd2f03b..904bcfc 100755 --- a/deploy/db/migrate.sh +++ b/deploy/db/migrate.sh @@ -64,7 +64,7 @@ failed_cleanup() { trap 'if [ $? -ne 0 ]; then failed_cleanup; fi' EXIT echo "Migration in progress from $CUR_VER to $TGT_VER" -${MIGRATION_BIN}/prmigrate-${ENVR}-${COMMIT_HASH} $UPDATED_BACKUP $CMD $TGT_VER +${MIGRATION_BIN}/migrate-${ENVR}-${COMMIT_HASH} $UPDATED_BACKUP $CMD $TGT_VER if [ $? -ne 0 ]; then echo "Migration failed" exit 1 diff --git a/go.mod b/go.mod index f15482c..6e65741 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 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 diff --git a/go.sum b/go.sum index b4bb05c..4e74dcf 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= diff --git a/handler/account.go b/internal/handler/account.go similarity index 93% rename from handler/account.go rename to internal/handler/account.go index 5236e1e..fcb97e8 100644 --- a/handler/account.go +++ b/internal/handler/account.go @@ -5,11 +5,12 @@ import ( "net/http" "time" - "projectreshoot/contexts" - "projectreshoot/cookies" - "projectreshoot/db" - "projectreshoot/view/component/account" - "projectreshoot/view/page" + "projectreshoot/internal/models" + "projectreshoot/internal/view/component/account" + "projectreshoot/internal/view/page" + "projectreshoot/pkg/contexts" + "projectreshoot/pkg/cookies" + "projectreshoot/pkg/db" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -60,7 +61,7 @@ func ChangeUsername( } r.ParseForm() newUsername := r.FormValue("username") - unique, err := db.CheckUsernameUnique(ctx, tx, newUsername) + unique, err := models.CheckUsernameUnique(ctx, tx, newUsername) if err != nil { tx.Rollback() logger.Error().Err(err).Msg("Error updating username") @@ -127,8 +128,6 @@ func ChangeBio( ) } func validateChangePassword( - ctx context.Context, - tx *db.SafeTX, r *http.Request, ) (string, error) { r.ParseForm() @@ -160,7 +159,7 @@ func ChangePassword( w.WriteHeader(http.StatusServiceUnavailable) return } - newPass, err := validateChangePassword(ctx, tx, r) + newPass, err := validateChangePassword(r) if err != nil { tx.Rollback() account.ChangePassword(err.Error()).Render(r.Context(), w) diff --git a/handler/errorpage.go b/internal/handler/errorpage.go similarity index 94% rename from handler/errorpage.go rename to internal/handler/errorpage.go index 67fe951..4632cf9 100644 --- a/handler/errorpage.go +++ b/internal/handler/errorpage.go @@ -2,7 +2,7 @@ package handler import ( "net/http" - "projectreshoot/view/page" + "projectreshoot/internal/view/page" ) func ErrorPage( diff --git a/handler/index.go b/internal/handler/index.go similarity index 91% rename from handler/index.go rename to internal/handler/index.go index f7e09bf..a378b3c 100644 --- a/handler/index.go +++ b/internal/handler/index.go @@ -3,7 +3,7 @@ package handler import ( "net/http" - "projectreshoot/view/page" + "projectreshoot/internal/view/page" ) // Handles responses to the / path. Also serves a 404 Page for paths that diff --git a/handler/login.go b/internal/handler/login.go similarity index 89% rename from handler/login.go rename to internal/handler/login.go index f366447..8157db3 100644 --- a/handler/login.go +++ b/internal/handler/login.go @@ -5,11 +5,12 @@ import ( "net/http" "time" - "projectreshoot/config" - "projectreshoot/cookies" - "projectreshoot/db" - "projectreshoot/view/component/form" - "projectreshoot/view/page" + "projectreshoot/internal/models" + "projectreshoot/internal/view/component/form" + "projectreshoot/internal/view/page" + "projectreshoot/pkg/config" + "projectreshoot/pkg/cookies" + "projectreshoot/pkg/db" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -19,12 +20,12 @@ import ( // is correct. Returns the corresponding user func validateLogin( ctx context.Context, - tx *db.SafeTX, + tx db.SafeTX, r *http.Request, -) (*db.User, error) { +) (*models.User, error) { formUsername := r.FormValue("username") formPassword := r.FormValue("password") - user, err := db.GetUserFromUsername(ctx, tx, formUsername) + user, err := models.GetUserFromUsername(ctx, tx, formUsername) if err != nil { return nil, errors.Wrap(err, "db.GetUserFromUsername") } diff --git a/handler/logout.go b/internal/handler/logout.go similarity index 94% rename from handler/logout.go rename to internal/handler/logout.go index 7a14572..715c81f 100644 --- a/handler/logout.go +++ b/internal/handler/logout.go @@ -6,10 +6,10 @@ import ( "strings" "time" - "projectreshoot/config" - "projectreshoot/cookies" - "projectreshoot/db" - "projectreshoot/jwt" + "projectreshoot/pkg/config" + "projectreshoot/pkg/cookies" + "projectreshoot/pkg/db" + "projectreshoot/pkg/jwt" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -18,7 +18,7 @@ import ( func revokeAccess( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx *db.SafeWTX, atStr string, ) error { aT, err := jwt.ParseAccessToken(config, ctx, tx, atStr) @@ -39,7 +39,7 @@ func revokeAccess( func revokeRefresh( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx *db.SafeWTX, rtStr string, ) error { rT, err := jwt.ParseRefreshToken(config, ctx, tx, rtStr) @@ -61,7 +61,7 @@ func revokeRefresh( func revokeTokens( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx *db.SafeWTX, r *http.Request, ) error { // get the tokens from the cookies diff --git a/handler/movie.go b/internal/handler/movie.go similarity index 92% rename from handler/movie.go rename to internal/handler/movie.go index b312d39..c4b286e 100644 --- a/handler/movie.go +++ b/internal/handler/movie.go @@ -2,9 +2,9 @@ package handler import ( "net/http" - "projectreshoot/config" - "projectreshoot/tmdb" - "projectreshoot/view/page" + "projectreshoot/internal/view/page" + "projectreshoot/pkg/config" + "projectreshoot/pkg/tmdb" "strconv" "github.com/rs/zerolog" diff --git a/handler/movie_search.go b/internal/handler/movie_search.go similarity index 84% rename from handler/movie_search.go rename to internal/handler/movie_search.go index bb19b11..aaa425a 100644 --- a/handler/movie_search.go +++ b/internal/handler/movie_search.go @@ -2,10 +2,10 @@ package handler import ( "net/http" - "projectreshoot/config" - "projectreshoot/tmdb" - "projectreshoot/view/component/search" - "projectreshoot/view/page" + "projectreshoot/internal/view/component/search" + "projectreshoot/internal/view/page" + "projectreshoot/pkg/config" + "projectreshoot/pkg/tmdb" "github.com/rs/zerolog" ) diff --git a/handler/page.go b/internal/handler/page.go similarity index 100% rename from handler/page.go rename to internal/handler/page.go diff --git a/handler/profile.go b/internal/handler/profile.go similarity index 84% rename from handler/profile.go rename to internal/handler/profile.go index 51763ea..ad85283 100644 --- a/handler/profile.go +++ b/internal/handler/profile.go @@ -2,7 +2,7 @@ package handler import ( "net/http" - "projectreshoot/view/page" + "projectreshoot/internal/view/page" ) func ProfilePage() http.Handler { diff --git a/handler/reauthenticatate.go b/internal/handler/reauthenticatate.go similarity index 92% rename from handler/reauthenticatate.go rename to internal/handler/reauthenticatate.go index 6bf7317..b3e8189 100644 --- a/handler/reauthenticatate.go +++ b/internal/handler/reauthenticatate.go @@ -5,12 +5,12 @@ import ( "net/http" "time" - "projectreshoot/config" - "projectreshoot/contexts" - "projectreshoot/cookies" - "projectreshoot/db" - "projectreshoot/jwt" - "projectreshoot/view/component/form" + "projectreshoot/internal/view/component/form" + "projectreshoot/pkg/config" + "projectreshoot/pkg/contexts" + "projectreshoot/pkg/cookies" + "projectreshoot/pkg/db" + "projectreshoot/pkg/jwt" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -20,7 +20,7 @@ import ( func getTokens( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx db.SafeTX, r *http.Request, ) (*jwt.AccessToken, *jwt.RefreshToken, error) { // get the existing tokens from the cookies @@ -39,7 +39,7 @@ func getTokens( // Revoke the given token pair func revokeTokenPair( ctx context.Context, - tx *db.SafeTX, + tx *db.SafeWTX, aT *jwt.AccessToken, rT *jwt.RefreshToken, ) error { @@ -58,7 +58,7 @@ func revokeTokenPair( func refreshTokens( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx *db.SafeWTX, w http.ResponseWriter, r *http.Request, ) error { diff --git a/handler/register.go b/internal/handler/register.go similarity index 86% rename from handler/register.go rename to internal/handler/register.go index 2599ef5..226b3b7 100644 --- a/handler/register.go +++ b/internal/handler/register.go @@ -5,11 +5,12 @@ import ( "net/http" "time" - "projectreshoot/config" - "projectreshoot/cookies" - "projectreshoot/db" - "projectreshoot/view/component/form" - "projectreshoot/view/page" + "projectreshoot/internal/models" + "projectreshoot/internal/view/component/form" + "projectreshoot/internal/view/page" + "projectreshoot/pkg/config" + "projectreshoot/pkg/cookies" + "projectreshoot/pkg/db" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -17,13 +18,13 @@ import ( func validateRegistration( ctx context.Context, - tx *db.SafeTX, + tx *db.SafeWTX, r *http.Request, -) (*db.User, error) { +) (*models.User, error) { formUsername := r.FormValue("username") formPassword := r.FormValue("password") formConfirmPassword := r.FormValue("confirm-password") - unique, err := db.CheckUsernameUnique(ctx, tx, formUsername) + unique, err := models.CheckUsernameUnique(ctx, tx, formUsername) if err != nil { return nil, errors.Wrap(err, "db.CheckUsernameUnique") } @@ -36,7 +37,7 @@ func validateRegistration( if len(formPassword) > 72 { return nil, errors.New("Password exceeds maximum length of 72 bytes") } - user, err := db.CreateNewUser(ctx, tx, formUsername, formPassword) + user, err := models.CreateNewUser(ctx, tx, formUsername, formPassword) if err != nil { return nil, errors.Wrap(err, "db.CreateNewUser") } diff --git a/handler/static.go b/internal/handler/static.go similarity index 100% rename from handler/static.go rename to internal/handler/static.go diff --git a/server/routes.go b/internal/httpserver/routes.go similarity index 91% rename from server/routes.go rename to internal/httpserver/routes.go index e99fd46..e31bc7a 100644 --- a/server/routes.go +++ b/internal/httpserver/routes.go @@ -1,13 +1,13 @@ -package server +package httpserver import ( "net/http" - "projectreshoot/config" - "projectreshoot/db" - "projectreshoot/handler" - "projectreshoot/middleware" - "projectreshoot/view/page" + "projectreshoot/internal/handler" + "projectreshoot/internal/middleware" + "projectreshoot/internal/view/page" + "projectreshoot/pkg/config" + "projectreshoot/pkg/db" "github.com/rs/zerolog" ) diff --git a/server/server.go b/internal/httpserver/server.go similarity index 55% rename from server/server.go rename to internal/httpserver/server.go index 8f189ad..f634505 100644 --- a/server/server.go +++ b/internal/httpserver/server.go @@ -1,17 +1,39 @@ -package server +package httpserver import ( + "io/fs" + "net" "net/http" + "time" - "projectreshoot/config" - "projectreshoot/db" - "projectreshoot/middleware" + "projectreshoot/internal/middleware" + "projectreshoot/pkg/config" + "projectreshoot/pkg/db" "github.com/rs/zerolog" ) -// Returns a new http.Handler with all the routes and middleware added func NewServer( + config *config.Config, + logger *zerolog.Logger, + conn *db.SafeConn, + staticFS *fs.FS, + maint *uint32, +) *http.Server { + fs := http.FS(*staticFS) + srv := createServer(config, logger, conn, &fs, maint) + httpServer := &http.Server{ + Addr: net.JoinHostPort(config.Host, config.Port), + Handler: srv, + ReadHeaderTimeout: config.ReadHeaderTimeout * time.Second, + WriteTimeout: config.WriteTimeout * time.Second, + IdleTimeout: config.IdleTimeout * time.Second, + } + return httpServer +} + +// Returns a new http.Handler with all the routes and middleware added +func createServer( config *config.Config, logger *zerolog.Logger, conn *db.SafeConn, diff --git a/middleware/authentication.go b/internal/middleware/authentication.go similarity index 92% rename from middleware/authentication.go rename to internal/middleware/authentication.go index 6c192ea..4393eff 100644 --- a/middleware/authentication.go +++ b/internal/middleware/authentication.go @@ -6,12 +6,13 @@ import ( "sync/atomic" "time" - "projectreshoot/config" - "projectreshoot/contexts" - "projectreshoot/cookies" - "projectreshoot/db" - "projectreshoot/handler" - "projectreshoot/jwt" + "projectreshoot/internal/handler" + "projectreshoot/internal/models" + "projectreshoot/pkg/config" + "projectreshoot/pkg/contexts" + "projectreshoot/pkg/cookies" + "projectreshoot/pkg/db" + "projectreshoot/pkg/jwt" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -21,11 +22,11 @@ import ( func refreshAuthTokens( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx *db.SafeWTX, w http.ResponseWriter, req *http.Request, ref *jwt.RefreshToken, -) (*db.User, error) { +) (*models.User, error) { user, err := ref.GetUser(ctx, tx) if err != nil { return nil, errors.Wrap(err, "ref.GetUser") @@ -54,7 +55,7 @@ func refreshAuthTokens( func getAuthenticatedUser( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx *db.SafeWTX, w http.ResponseWriter, r *http.Request, ) (*contexts.AuthenticatedUser, error) { diff --git a/middleware/authentication_test.go b/internal/middleware/authentication_test.go similarity index 97% rename from middleware/authentication_test.go rename to internal/middleware/authentication_test.go index bb3dceb..df68ad0 100644 --- a/middleware/authentication_test.go +++ b/internal/middleware/authentication_test.go @@ -8,9 +8,9 @@ import ( "sync/atomic" "testing" - "projectreshoot/contexts" - "projectreshoot/db" - "projectreshoot/tests" + "projectreshoot/pkg/contexts" + "projectreshoot/pkg/db" + "projectreshoot/pkg/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -22,9 +22,9 @@ func TestAuthenticationMiddleware(t *testing.T) { logger := tests.NilLogger() ver, err := strconv.ParseInt(cfg.DBName, 10, 0) require.NoError(t, err) - conn, err := tests.SetupTestDB(ver) + wconn, rconn, err := tests.SetupTestDB(ver) require.NoError(t, err) - sconn := db.MakeSafe(conn, logger) + sconn := db.MakeSafe(wconn, rconn, logger) defer sconn.Close() // Handler to check outcome of Authentication middleware diff --git a/middleware/gzip.go b/internal/middleware/gzip.go similarity index 100% rename from middleware/gzip.go rename to internal/middleware/gzip.go diff --git a/middleware/logging.go b/internal/middleware/logging.go similarity index 94% rename from middleware/logging.go rename to internal/middleware/logging.go index ae6616b..e890152 100644 --- a/middleware/logging.go +++ b/internal/middleware/logging.go @@ -2,8 +2,8 @@ package middleware import ( "net/http" - "projectreshoot/contexts" - "projectreshoot/handler" + "projectreshoot/internal/handler" + "projectreshoot/pkg/contexts" "time" "github.com/rs/zerolog" diff --git a/middleware/pageprotection.go b/internal/middleware/pageprotection.go similarity index 92% rename from middleware/pageprotection.go rename to internal/middleware/pageprotection.go index 7f104c0..c2442e5 100644 --- a/middleware/pageprotection.go +++ b/internal/middleware/pageprotection.go @@ -2,8 +2,8 @@ package middleware import ( "net/http" - "projectreshoot/contexts" - "projectreshoot/handler" + "projectreshoot/internal/handler" + "projectreshoot/pkg/contexts" ) // Checks if the user is set in the context and shows 401 page if not logged in diff --git a/middleware/pageprotection_test.go b/internal/middleware/pageprotection_test.go similarity index 93% rename from middleware/pageprotection_test.go rename to internal/middleware/pageprotection_test.go index 93414f8..8cbc81b 100644 --- a/middleware/pageprotection_test.go +++ b/internal/middleware/pageprotection_test.go @@ -7,8 +7,8 @@ import ( "sync/atomic" "testing" - "projectreshoot/db" - "projectreshoot/tests" + "projectreshoot/pkg/db" + "projectreshoot/pkg/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,9 +20,9 @@ func TestPageLoginRequired(t *testing.T) { logger := tests.NilLogger() ver, err := strconv.ParseInt(cfg.DBName, 10, 0) require.NoError(t, err) - conn, err := tests.SetupTestDB(ver) + wconn, rconn, err := tests.SetupTestDB(ver) require.NoError(t, err) - sconn := db.MakeSafe(conn, logger) + sconn := db.MakeSafe(wconn, rconn, logger) defer sconn.Close() // Handler to check outcome of Authentication middleware diff --git a/middleware/reauthentication.go b/internal/middleware/reauthentication.go similarity index 91% rename from middleware/reauthentication.go rename to internal/middleware/reauthentication.go index b1fdb93..a5dcf00 100644 --- a/middleware/reauthentication.go +++ b/internal/middleware/reauthentication.go @@ -2,7 +2,7 @@ package middleware import ( "net/http" - "projectreshoot/contexts" + "projectreshoot/pkg/contexts" "time" ) diff --git a/middleware/reauthentication_test.go b/internal/middleware/reauthentication_test.go similarity index 93% rename from middleware/reauthentication_test.go rename to internal/middleware/reauthentication_test.go index 646a557..de407e0 100644 --- a/middleware/reauthentication_test.go +++ b/internal/middleware/reauthentication_test.go @@ -7,8 +7,8 @@ import ( "sync/atomic" "testing" - "projectreshoot/db" - "projectreshoot/tests" + "projectreshoot/pkg/db" + "projectreshoot/pkg/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,9 +20,9 @@ func TestReauthRequired(t *testing.T) { logger := tests.NilLogger() ver, err := strconv.ParseInt(cfg.DBName, 10, 0) require.NoError(t, err) - conn, err := tests.SetupTestDB(ver) + wconn, rconn, err := tests.SetupTestDB(ver) require.NoError(t, err) - sconn := db.MakeSafe(conn, logger) + sconn := db.MakeSafe(wconn, rconn, logger) defer sconn.Close() // Handler to check outcome of Authentication middleware diff --git a/middleware/start.go b/internal/middleware/start.go similarity index 91% rename from middleware/start.go rename to internal/middleware/start.go index fb9a66c..f0235b1 100644 --- a/middleware/start.go +++ b/internal/middleware/start.go @@ -2,7 +2,7 @@ package middleware import ( "net/http" - "projectreshoot/contexts" + "projectreshoot/pkg/contexts" "time" ) diff --git a/db/user.go b/internal/models/user.go similarity index 83% rename from db/user.go rename to internal/models/user.go index a2daa26..47a2341 100644 --- a/db/user.go +++ b/internal/models/user.go @@ -1,7 +1,8 @@ -package db +package models import ( "context" + "projectreshoot/pkg/db" "github.com/pkg/errors" "golang.org/x/crypto/bcrypt" @@ -16,7 +17,7 @@ type User struct { } // Uses bcrypt to set the users Password_hash from the given password -func (user *User) SetPassword(ctx context.Context, tx *SafeTX, password string) error { +func (user *User) SetPassword(ctx context.Context, tx *db.SafeWTX, password string) error { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return errors.Wrap(err, "bcrypt.GenerateFromPassword") @@ -40,7 +41,7 @@ func (user *User) CheckPassword(password string) error { } // Change the user's username -func (user *User) ChangeUsername(ctx context.Context, tx *SafeTX, newUsername string) error { +func (user *User) ChangeUsername(ctx context.Context, tx *db.SafeWTX, newUsername string) error { query := `UPDATE users SET username = ? WHERE id = ?` _, err := tx.Exec(ctx, query, newUsername, user.ID) if err != nil { @@ -50,7 +51,7 @@ func (user *User) ChangeUsername(ctx context.Context, tx *SafeTX, newUsername st } // Change the user's bio -func (user *User) ChangeBio(ctx context.Context, tx *SafeTX, newBio string) error { +func (user *User) ChangeBio(ctx context.Context, tx *db.SafeWTX, newBio string) error { query := `UPDATE users SET bio = ? WHERE id = ?` _, err := tx.Exec(ctx, query, newBio, user.ID) if err != nil { diff --git a/db/user_functions.go b/internal/models/user_functions.go similarity index 88% rename from db/user_functions.go rename to internal/models/user_functions.go index 6dd76d4..2bfea8a 100644 --- a/db/user_functions.go +++ b/internal/models/user_functions.go @@ -1,9 +1,10 @@ -package db +package models import ( "context" "database/sql" "fmt" + "projectreshoot/pkg/db" "github.com/pkg/errors" ) @@ -11,7 +12,7 @@ import ( // Creates a new user in the database and returns a pointer func CreateNewUser( ctx context.Context, - tx *SafeTX, + tx *db.SafeWTX, username string, password string, ) (*User, error) { @@ -34,7 +35,7 @@ func CreateNewUser( // Fetches data from the users table using "WHERE column = 'value'" func fetchUserData( ctx context.Context, - tx *SafeTX, + tx db.SafeTX, column string, value interface{}, ) (*sql.Rows, error) { @@ -77,7 +78,7 @@ 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 *SafeTX, username string) (*User, error) { +func GetUserFromUsername(ctx context.Context, tx db.SafeTX, username string) (*User, error) { rows, err := fetchUserData(ctx, tx, "username", username) if err != nil { return nil, errors.Wrap(err, "fetchUserData") @@ -92,7 +93,7 @@ func GetUserFromUsername(ctx context.Context, tx *SafeTX, username string) (*Use } // Queries the database for a user matching the given ID. -func GetUserFromID(ctx context.Context, tx *SafeTX, id int) (*User, error) { +func GetUserFromID(ctx context.Context, tx db.SafeTX, id int) (*User, error) { rows, err := fetchUserData(ctx, tx, "id", id) if err != nil { return nil, errors.Wrap(err, "fetchUserData") @@ -107,7 +108,7 @@ func GetUserFromID(ctx context.Context, tx *SafeTX, id int) (*User, error) { } // Checks if the given username is unique. Returns true if not taken -func CheckUsernameUnique(ctx context.Context, tx *SafeTX, username string) (bool, error) { +func CheckUsernameUnique(ctx context.Context, tx db.SafeTX, username string) (bool, error) { query := `SELECT 1 FROM users WHERE username = ? COLLATE NOCASE LIMIT 1` rows, err := tx.Query(ctx, query, username) if err != nil { diff --git a/view/component/account/changebio.templ b/internal/view/component/account/changebio.templ similarity index 98% rename from view/component/account/changebio.templ rename to internal/view/component/account/changebio.templ index cb85013..68e42d8 100644 --- a/view/component/account/changebio.templ +++ b/internal/view/component/account/changebio.templ @@ -1,6 +1,6 @@ package account -import "projectreshoot/contexts" +import "projectreshoot/pkg/contexts" templ ChangeBio(err string, bio string) { {{ diff --git a/internal/view/component/account/changebio_templ.go b/internal/view/component/account/changebio_templ.go new file mode 100644 index 0000000..935782e --- /dev/null +++ b/internal/view/component/account/changebio_templ.go @@ -0,0 +1,60 @@ +// 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/account/changepassword.templ b/internal/view/component/account/changepassword.templ similarity index 100% rename from view/component/account/changepassword.templ rename to internal/view/component/account/changepassword.templ diff --git a/internal/view/component/account/changepassword_templ.go b/internal/view/component/account/changepassword_templ.go new file mode 100644 index 0000000..4808626 --- /dev/null +++ b/internal/view/component/account/changepassword_templ.go @@ -0,0 +1,55 @@ +// 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/account/changeusername.templ b/internal/view/component/account/changeusername.templ similarity index 98% rename from view/component/account/changeusername.templ rename to internal/view/component/account/changeusername.templ index 25cc151..6c2895e 100644 --- a/view/component/account/changeusername.templ +++ b/internal/view/component/account/changeusername.templ @@ -1,6 +1,6 @@ package account -import "projectreshoot/contexts" +import "projectreshoot/pkg/contexts" templ ChangeUsername(err string, username string) { {{ diff --git a/internal/view/component/account/changeusername_templ.go b/internal/view/component/account/changeusername_templ.go new file mode 100644 index 0000000..22f647b --- /dev/null +++ b/internal/view/component/account/changeusername_templ.go @@ -0,0 +1,62 @@ +// 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/account/container.templ b/internal/view/component/account/container.templ similarity index 100% rename from view/component/account/container.templ rename to internal/view/component/account/container.templ diff --git a/internal/view/component/account/container_templ.go b/internal/view/component/account/container_templ.go new file mode 100644 index 0000000..5cb89da --- /dev/null +++ b/internal/view/component/account/container_templ.go @@ -0,0 +1,77 @@ +// 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, "
") + 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, "
") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/account/general.templ b/internal/view/component/account/general.templ similarity index 100% rename from view/component/account/general.templ rename to internal/view/component/account/general.templ diff --git a/internal/view/component/account/general_templ.go b/internal/view/component/account/general_templ.go new file mode 100644 index 0000000..39d9a7a --- /dev/null +++ b/internal/view/component/account/general_templ.go @@ -0,0 +1,52 @@ +// 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/account/security.templ b/internal/view/component/account/security.templ similarity index 100% rename from view/component/account/security.templ rename to internal/view/component/account/security.templ diff --git a/internal/view/component/account/security_templ.go b/internal/view/component/account/security_templ.go new file mode 100644 index 0000000..a0b15e5 --- /dev/null +++ b/internal/view/component/account/security_templ.go @@ -0,0 +1,48 @@ +// 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/account/selectmenu.templ b/internal/view/component/account/selectmenu.templ similarity index 100% rename from view/component/account/selectmenu.templ rename to internal/view/component/account/selectmenu.templ diff --git a/internal/view/component/account/selectmenu_templ.go b/internal/view/component/account/selectmenu_templ.go new file mode 100644 index 0000000..86021b5 --- /dev/null +++ b/internal/view/component/account/selectmenu_templ.go @@ -0,0 +1,131 @@ +// 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, "
    ") + 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, "
  • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/footer/footer.templ b/internal/view/component/footer/footer.templ similarity index 100% rename from view/component/footer/footer.templ rename to internal/view/component/footer/footer.templ diff --git a/internal/view/component/footer/footer_templ.go b/internal/view/component/footer/footer_templ.go new file mode 100644 index 0000000..359e1ff --- /dev/null +++ b/internal/view/component/footer/footer_templ.go @@ -0,0 +1,92 @@ +// 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/form/confirmpass.templ b/internal/view/component/form/confirmpass.templ similarity index 100% rename from view/component/form/confirmpass.templ rename to internal/view/component/form/confirmpass.templ diff --git a/internal/view/component/form/confirmpass_templ.go b/internal/view/component/form/confirmpass_templ.go new file mode 100644 index 0000000..ceed0b2 --- /dev/null +++ b/internal/view/component/form/confirmpass_templ.go @@ -0,0 +1,55 @@ +// 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/form/loginform.templ b/internal/view/component/form/loginform.templ similarity index 100% rename from view/component/form/loginform.templ rename to internal/view/component/form/loginform.templ diff --git a/internal/view/component/form/loginform_templ.go b/internal/view/component/form/loginform_templ.go new file mode 100644 index 0000000..e8bc085 --- /dev/null +++ b/internal/view/component/form/loginform_templ.go @@ -0,0 +1,60 @@ +// 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/form/registerform.templ b/internal/view/component/form/registerform.templ similarity index 100% rename from view/component/form/registerform.templ rename to internal/view/component/form/registerform.templ diff --git a/internal/view/component/form/registerform_templ.go b/internal/view/component/form/registerform_templ.go new file mode 100644 index 0000000..d332b3f --- /dev/null +++ b/internal/view/component/form/registerform_templ.go @@ -0,0 +1,63 @@ +// 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/nav/navbar.templ b/internal/view/component/nav/navbar.templ similarity index 100% rename from view/component/nav/navbar.templ rename to internal/view/component/nav/navbar.templ diff --git a/internal/view/component/nav/navbar_templ.go b/internal/view/component/nav/navbar_templ.go new file mode 100644 index 0000000..7e7b2f8 --- /dev/null +++ b/internal/view/component/nav/navbar_templ.go @@ -0,0 +1,77 @@ +// 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, "
Project Reshoot
") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/nav/navbarleft.templ b/internal/view/component/nav/navbarleft.templ similarity index 100% rename from view/component/nav/navbarleft.templ rename to internal/view/component/nav/navbarleft.templ diff --git a/internal/view/component/nav/navbarleft_templ.go b/internal/view/component/nav/navbarleft_templ.go new file mode 100644 index 0000000..119bb65 --- /dev/null +++ b/internal/view/component/nav/navbarleft_templ.go @@ -0,0 +1,73 @@ +// 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/nav/navbarright.templ b/internal/view/component/nav/navbarright.templ similarity index 98% rename from view/component/nav/navbarright.templ rename to internal/view/component/nav/navbarright.templ index 4d3a114..2f7f767 100644 --- a/view/component/nav/navbarright.templ +++ b/internal/view/component/nav/navbarright.templ @@ -1,6 +1,6 @@ package nav -import "projectreshoot/contexts" +import "projectreshoot/pkg/contexts" type ProfileItem struct { name string // Label to display diff --git a/internal/view/component/nav/navbarright_templ.go b/internal/view/component/nav/navbarright_templ.go new file mode 100644 index 0000000..98515bc --- /dev/null +++ b/internal/view/component/nav/navbarright_templ.go @@ -0,0 +1,124 @@ +// 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if user != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Login Register") + 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 + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/nav/sidenav.templ b/internal/view/component/nav/sidenav.templ similarity index 97% rename from view/component/nav/sidenav.templ rename to internal/view/component/nav/sidenav.templ index b4bfd62..f8b6243 100644 --- a/view/component/nav/sidenav.templ +++ b/internal/view/component/nav/sidenav.templ @@ -1,6 +1,6 @@ package nav -import "projectreshoot/contexts" +import "projectreshoot/pkg/contexts" // Returns the mobile version of the navbar thats only visible when activated templ sideNav(navItems []NavItem) { diff --git a/internal/view/component/nav/sidenav_templ.go b/internal/view/component/nav/sidenav_templ.go new file mode 100644 index 0000000..3fb57d6 --- /dev/null +++ b/internal/view/component/nav/sidenav_templ.go @@ -0,0 +1,86 @@ +// 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if user == nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/popup/confirmPasswordModal.templ b/internal/view/component/popup/confirmPasswordModal.templ similarity index 88% rename from view/component/popup/confirmPasswordModal.templ rename to internal/view/component/popup/confirmPasswordModal.templ index 7c2daef..bbe58a4 100644 --- a/view/component/popup/confirmPasswordModal.templ +++ b/internal/view/component/popup/confirmPasswordModal.templ @@ -1,7 +1,7 @@ package popup -import "projectreshoot/view/component/form" +import "projectreshoot/internal/view/component/form" templ ConfirmPasswordModal() {
To complete this action you need to confirm your password
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/popup/error500Popup.templ b/internal/view/component/popup/error500Popup.templ similarity index 100% rename from view/component/popup/error500Popup.templ rename to internal/view/component/popup/error500Popup.templ diff --git a/internal/view/component/popup/error500Popup_templ.go b/internal/view/component/popup/error500Popup_templ.go new file mode 100644 index 0000000..cd21eef --- /dev/null +++ b/internal/view/component/popup/error500Popup_templ.go @@ -0,0 +1,40 @@ +// 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, "
Something went wrong

An error occured on the server. Please try again later, or contact an administrator

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/popup/error503Popup.templ b/internal/view/component/popup/error503Popup.templ similarity index 100% rename from view/component/popup/error503Popup.templ rename to internal/view/component/popup/error503Popup.templ diff --git a/internal/view/component/popup/error503Popup_templ.go b/internal/view/component/popup/error503Popup_templ.go new file mode 100644 index 0000000..e542023 --- /dev/null +++ b/internal/view/component/popup/error503Popup_templ.go @@ -0,0 +1,40 @@ +// 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, "
Service Unavailable

The service is currently available. It could be down for maintenance. Please try again later.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/component/search/movies_results.templ b/internal/view/component/search/movies_results.templ similarity index 97% rename from view/component/search/movies_results.templ rename to internal/view/component/search/movies_results.templ index 396a6fa..d8d8e7a 100644 --- a/view/component/search/movies_results.templ +++ b/internal/view/component/search/movies_results.templ @@ -1,6 +1,6 @@ package search -import "projectreshoot/tmdb" +import "projectreshoot/pkg/tmdb" import "fmt" templ MovieResults(movies *tmdb.ResultMovies, image *tmdb.Image) { diff --git a/internal/view/component/search/movies_results_templ.go b/internal/view/component/search/movies_results_templ.go new file mode 100644 index 0000000..70ca412 --- /dev/null +++ b/internal/view/component/search/movies_results_templ.go @@ -0,0 +1,132 @@ +// 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, "
\"Movie
") + 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, "

Released: ") + 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, "

Original Title: ") + 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, "

") + 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, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/layout/global.templ b/internal/view/layout/global.templ similarity index 96% rename from view/layout/global.templ rename to internal/view/layout/global.templ index 95536f2..b099138 100644 --- a/view/layout/global.templ +++ b/internal/view/layout/global.templ @@ -1,8 +1,8 @@ package layout -import "projectreshoot/view/component/nav" -import "projectreshoot/view/component/footer" -import "projectreshoot/view/component/popup" +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 diff --git a/internal/view/layout/global_templ.go b/internal/view/layout/global_templ.go new file mode 100644 index 0000000..cfca892 --- /dev/null +++ b/internal/view/layout/global_templ.go @@ -0,0 +1,99 @@ +// 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, "") + 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, "") + 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, "
") + 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, "
") + 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, "
") + 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/view/page/about.templ b/internal/view/page/about.templ similarity index 97% rename from view/page/about.templ rename to internal/view/page/about.templ index 66b28e8..9c05aaa 100644 --- a/view/page/about.templ +++ b/internal/view/page/about.templ @@ -1,6 +1,6 @@ package page -import "projectreshoot/view/layout" +import "projectreshoot/internal/view/layout" // Returns the about page content templ About() { diff --git a/internal/view/page/about_templ.go b/internal/view/page/about_templ.go new file mode 100644 index 0000000..f449951 --- /dev/null +++ b/internal/view/page/about_templ.go @@ -0,0 +1,61 @@ +// 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, "
About
What is Project Reshoot?
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.
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.
Why the name?
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).
Who's behind it?
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 Github repo here.
") + 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 diff --git a/view/page/account.templ b/internal/view/page/account.templ similarity index 56% rename from view/page/account.templ rename to internal/view/page/account.templ index f4aa29a..ed9356d 100644 --- a/view/page/account.templ +++ b/internal/view/page/account.templ @@ -1,7 +1,7 @@ package page -import "projectreshoot/view/layout" -import "projectreshoot/view/component/account" +import "projectreshoot/internal/view/layout" +import "projectreshoot/internal/view/component/account" templ Account(subpage string) { @layout.Global("Account - " + subpage) { diff --git a/internal/view/page/account_templ.go b/internal/view/page/account_templ.go new file mode 100644 index 0000000..3f8c7fa --- /dev/null +++ b/internal/view/page/account_templ.go @@ -0,0 +1,61 @@ +// 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 diff --git a/view/page/error.templ b/internal/view/page/error.templ similarity index 95% rename from view/page/error.templ rename to internal/view/page/error.templ index 5da21ec..b40aa33 100644 --- a/view/page/error.templ +++ b/internal/view/page/error.templ @@ -1,6 +1,6 @@ package page -import "projectreshoot/view/layout" +import "projectreshoot/internal/view/layout" import "strconv" // Page template for Error pages. Error code should be a HTTP status code as diff --git a/internal/view/page/error_templ.go b/internal/view/page/error_templ.go new file mode 100644 index 0000000..9a3f240 --- /dev/null +++ b/internal/view/page/error_templ.go @@ -0,0 +1,103 @@ +// 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, "

") + 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, "

") + 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, "

") + 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, "

Go to homepage
") + 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 diff --git a/view/page/index.templ b/internal/view/page/index.templ similarity index 85% rename from view/page/index.templ rename to internal/view/page/index.templ index 350db4f..bdf891d 100644 --- a/view/page/index.templ +++ b/internal/view/page/index.templ @@ -1,6 +1,6 @@ package page -import "projectreshoot/view/layout" +import "projectreshoot/internal/view/layout" // Page content for the index page templ Index() { diff --git a/internal/view/page/index_templ.go b/internal/view/page/index_templ.go new file mode 100644 index 0000000..1e53b57 --- /dev/null +++ b/internal/view/page/index_templ.go @@ -0,0 +1,61 @@ +// 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, "
Project Reshoot
A better way to discover and rate films
") + 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 diff --git a/view/page/login.templ b/internal/view/page/login.templ similarity index 91% rename from view/page/login.templ rename to internal/view/page/login.templ index 1335601..0cd284d 100644 --- a/view/page/login.templ +++ b/internal/view/page/login.templ @@ -1,7 +1,7 @@ package page -import "projectreshoot/view/layout" -import "projectreshoot/view/component/form" +import "projectreshoot/internal/view/layout" +import "projectreshoot/internal/view/component/form" // Returns the login page templ Login() { diff --git a/internal/view/page/login_templ.go b/internal/view/page/login_templ.go new file mode 100644 index 0000000..923ea97 --- /dev/null +++ b/internal/view/page/login_templ.go @@ -0,0 +1,70 @@ +// 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, "

Login

Don't have an account yet? Sign up here

Or
") + 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, "
") + 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 diff --git a/view/page/movie.templ b/internal/view/page/movie.templ similarity index 97% rename from view/page/movie.templ rename to internal/view/page/movie.templ index ef7700b..3bc38ae 100644 --- a/view/page/movie.templ +++ b/internal/view/page/movie.templ @@ -1,7 +1,7 @@ package page -import "projectreshoot/tmdb" -import "projectreshoot/view/layout" +import "projectreshoot/pkg/tmdb" +import "projectreshoot/internal/view/layout" templ Movie(movie *tmdb.Movie, credits *tmdb.Credits, image *tmdb.Image) { @layout.Global(movie.Title) { diff --git a/view/page/movie_search.templ b/internal/view/page/movie_search.templ similarity index 94% rename from view/page/movie_search.templ rename to internal/view/page/movie_search.templ index c9bf9ff..65c23bc 100644 --- a/view/page/movie_search.templ +++ b/internal/view/page/movie_search.templ @@ -1,6 +1,6 @@ package page -import "projectreshoot/view/layout" +import "projectreshoot/internal/view/layout" templ Movies() { @layout.Global("Search movies") { diff --git a/internal/view/page/movie_search_templ.go b/internal/view/page/movie_search_templ.go new file mode 100644 index 0000000..5d82b0e --- /dev/null +++ b/internal/view/page/movie_search_templ.go @@ -0,0 +1,60 @@ +// 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, "
") + 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 diff --git a/internal/view/page/movie_templ.go b/internal/view/page/movie_templ.go new file mode 100644 index 0000000..f3adedd --- /dev/null +++ b/internal/view/page/movie_templ.go @@ -0,0 +1,188 @@ +// 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, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, billedcrew := range credits.BilledCrew() { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + 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, " ") + 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, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
\"Poster\"
") + 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, " ") + 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, "
") + 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, "
Overview ") + 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, "
") + 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 diff --git a/view/page/profile.templ b/internal/view/page/profile.templ similarity index 68% rename from view/page/profile.templ rename to internal/view/page/profile.templ index e85f6bc..7acffc0 100644 --- a/view/page/profile.templ +++ b/internal/view/page/profile.templ @@ -1,7 +1,7 @@ package page -import "projectreshoot/view/layout" -import "projectreshoot/contexts" +import "projectreshoot/internal/view/layout" +import "projectreshoot/pkg/contexts" templ Profile() { {{ user := contexts.GetUser(ctx) }} diff --git a/internal/view/page/profile_templ.go b/internal/view/page/profile_templ.go new file mode 100644 index 0000000..1a3b4ed --- /dev/null +++ b/internal/view/page/profile_templ.go @@ -0,0 +1,75 @@ +// 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, "
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, "
") + 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 diff --git a/view/page/register.templ b/internal/view/page/register.templ similarity index 91% rename from view/page/register.templ rename to internal/view/page/register.templ index a8e3bb1..cf7e222 100644 --- a/view/page/register.templ +++ b/internal/view/page/register.templ @@ -1,7 +1,7 @@ package page -import "projectreshoot/view/layout" -import "projectreshoot/view/component/form" +import "projectreshoot/internal/view/layout" +import "projectreshoot/internal/view/component/form" // Returns the login page templ Register() { diff --git a/internal/view/page/register_templ.go b/internal/view/page/register_templ.go new file mode 100644 index 0000000..d43a1e6 --- /dev/null +++ b/internal/view/page/register_templ.go @@ -0,0 +1,70 @@ +// 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, "

Register

Already have an account? Login here

Or
") + 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, "
") + 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 diff --git a/main.go b/main.go deleted file mode 100644 index 257de55..0000000 --- a/main.go +++ /dev/null @@ -1,233 +0,0 @@ -package main - -import ( - "context" - "embed" - "flag" - "fmt" - "io" - "io/fs" - "net" - "net/http" - "os" - "os/signal" - "strconv" - "sync" - "sync/atomic" - "syscall" - "time" - - "projectreshoot/config" - "projectreshoot/db" - "projectreshoot/logging" - "projectreshoot/server" - "projectreshoot/tests" - - "github.com/pkg/errors" - "github.com/rs/zerolog" -) - -//go:embed static/* -var embeddedStatic embed.FS - -// Gets the static files -func getStaticFiles(logger *zerolog.Logger) (http.FileSystem, error) { - if _, err := os.Stat("static"); err == nil { - // Use actual filesystem in development - logger.Debug().Msg("Using filesystem for static files") - return http.Dir("static"), nil - } else { - // Use embedded filesystem in production - logger.Debug().Msg("Using embedded static files") - subFS, err := fs.Sub(embeddedStatic, "static") - if err != nil { - return nil, errors.Wrap(err, "fs.Sub") - } - return http.FS(subFS), nil - } -} - -var maint uint32 // atomic: 1 if in maintenance mode - -// Handle SIGUSR1 and SIGUSR2 syscalls to toggle maintenance mode -func handleMaintSignals( - conn *db.SafeConn, - srv *http.Server, - logger *zerolog.Logger, - config *config.Config, -) { - logger.Debug().Msg("Starting signal listener") - ch := make(chan os.Signal, 1) - srv.RegisterOnShutdown(func() { - logger.Debug().Msg("Shutting down signal listener") - close(ch) - }) - go func() { - for sig := range ch { - switch sig { - case syscall.SIGUSR1: - 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) - } - } - } - }() - signal.Notify(ch, syscall.SIGUSR1, syscall.SIGUSR2) -} - -// Initializes and runs the server -func run(ctx context.Context, w io.Writer, args map[string]string) error { - ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) - defer cancel() - - config, err := config.GetConfig(args) - if err != nil { - return errors.Wrap(err, "server.GetConfig") - } - - // Return the version of the database required - if args["dbver"] == "true" { - fmt.Fprintf(w, "Database version: %s\n", config.DBName) - return nil - } - - var logfile *os.File = nil - if config.LogOutput == "both" || config.LogOutput == "file" { - logfile, err = logging.GetLogFile(config.LogDir) - if err != nil { - return errors.Wrap(err, "logging.GetLogFile") - } - defer logfile.Close() - } - - var consoleWriter io.Writer - if config.LogOutput == "both" || config.LogOutput == "console" { - consoleWriter = w - } - - logger, err := logging.GetLogger( - config.LogLevel, - consoleWriter, - logfile, - config.LogDir, - ) - if err != nil { - return errors.Wrap(err, "logging.GetLogger") - } - - logger.Debug().Msg("Config loaded and logger started") - logger.Debug().Msg("Connecting to database") - var conn *db.SafeConn - 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 errors.Wrap(err, "strconv.ParseInt") - } - testconn, err := tests.SetupTestDB(ver) - if err != nil { - return errors.Wrap(err, "tests.SetupTestDB") - } - conn = db.MakeSafe(testconn, logger) - } else { - conn, err = db.ConnectToDatabase(config.DBName, logger) - if err != nil { - return errors.Wrap(err, "db.ConnectToDatabase") - } - } - defer conn.Close() - - logger.Debug().Msg("Getting static files") - staticFS, err := getStaticFiles(logger) - if err != nil { - return errors.Wrap(err, "getStaticFiles") - } - - logger.Debug().Msg("Setting up HTTP server") - srv := server.NewServer(config, logger, conn, &staticFS, &maint) - httpServer := &http.Server{ - Addr: net.JoinHostPort(config.Host, config.Port), - Handler: srv, - ReadHeaderTimeout: config.ReadHeaderTimeout * time.Second, - WriteTimeout: config.WriteTimeout * time.Second, - IdleTimeout: config.IdleTimeout * time.Second, - } - - // Runs function for testing in dev if --test flag true - if args["tester"] == "true" { - logger.Debug().Msg("Running tester function") - test(config, logger, conn, httpServer) - return nil - } - - // Setups a channel to listen for os.Signal - handleMaintSignals(conn, httpServer, logger, config) - - // Runs the http server - logger.Debug().Msg("Starting up the HTTP server") - go func() { - logger.Info().Str("address", httpServer.Addr).Msg("Listening for requests") - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.Error().Err(err).Msg("Error listening and serving") - } - }() - - // Handles graceful shutdown - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - <-ctx.Done() - shutdownCtx := context.Background() - shutdownCtx, cancel := context.WithTimeout(shutdownCtx, 10*time.Second) - defer cancel() - 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 -} - -// Start of runtime. Parse commandline arguments & flags, Initializes context -// and starts the server -func main() { - // Parse commandline args - host := flag.String("host", "", "Override host to listen on") - port := flag.String("port", "", "Override port to listen on") - test := flag.Bool("test", false, "Run server in test mode") - tester := flag.Bool("tester", false, "Run tester function instead of main program") - dbver := flag.Bool("dbver", false, "Get the version of the database required") - loglevel := flag.String("loglevel", "", "Set log level") - logoutput := flag.String("logoutput", "", "Set log destination (file, console or both)") - flag.Parse() - - // Map the args for easy access - args := map[string]string{ - "host": *host, - "port": *port, - "test": strconv.FormatBool(*test), - "tester": strconv.FormatBool(*tester), - "dbver": strconv.FormatBool(*dbver), - "loglevel": *loglevel, - "logoutput": *logoutput, - } - - // Start the server - ctx := context.Background() - if err := run(ctx, os.Stdout, args); err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(1) - } -} diff --git a/config/config.go b/pkg/config/config.go similarity index 98% rename from config/config.go rename to pkg/config/config.go index f3e1012..7110e9d 100644 --- a/config/config.go +++ b/pkg/config/config.go @@ -5,8 +5,8 @@ import ( "os" "time" - "projectreshoot/logging" - "projectreshoot/tmdb" + "projectreshoot/pkg/logging" + "projectreshoot/pkg/tmdb" "github.com/joho/godotenv" "github.com/pkg/errors" diff --git a/config/environment.go b/pkg/config/environment.go similarity index 100% rename from config/environment.go rename to pkg/config/environment.go diff --git a/contexts/keys.go b/pkg/contexts/keys.go similarity index 100% rename from contexts/keys.go rename to pkg/contexts/keys.go diff --git a/contexts/request_timer.go b/pkg/contexts/request_timer.go similarity index 100% rename from contexts/request_timer.go rename to pkg/contexts/request_timer.go diff --git a/contexts/user.go b/pkg/contexts/user.go similarity index 91% rename from contexts/user.go rename to pkg/contexts/user.go index 0ef041f..c24e6bb 100644 --- a/contexts/user.go +++ b/pkg/contexts/user.go @@ -2,11 +2,11 @@ package contexts import ( "context" - "projectreshoot/db" + "projectreshoot/internal/models" ) type AuthenticatedUser struct { - *db.User + *models.User Fresh int64 } diff --git a/cookies/functions.go b/pkg/cookies/functions.go similarity index 100% rename from cookies/functions.go rename to pkg/cookies/functions.go diff --git a/cookies/pagefrom.go b/pkg/cookies/pagefrom.go similarity index 100% rename from cookies/pagefrom.go rename to pkg/cookies/pagefrom.go diff --git a/cookies/tokens.go b/pkg/cookies/tokens.go similarity index 93% rename from cookies/tokens.go rename to pkg/cookies/tokens.go index a5c20a0..696758c 100644 --- a/cookies/tokens.go +++ b/pkg/cookies/tokens.go @@ -4,9 +4,9 @@ import ( "net/http" "time" - "projectreshoot/config" - "projectreshoot/db" - "projectreshoot/jwt" + "projectreshoot/internal/models" + "projectreshoot/pkg/config" + "projectreshoot/pkg/jwt" "github.com/pkg/errors" ) @@ -58,7 +58,7 @@ func SetTokenCookies( w http.ResponseWriter, r *http.Request, config *config.Config, - user *db.User, + user *models.User, fresh bool, rememberMe bool, ) error { diff --git a/db/connection.go b/pkg/db/connection.go similarity index 65% rename from db/connection.go rename to pkg/db/connection.go index 67cdf22..a5eb718 100644 --- a/db/connection.go +++ b/pkg/db/connection.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" - _ "modernc.org/sqlite" + _ "github.com/mattn/go-sqlite3" ) // Returns a database connection handle for the DB @@ -16,20 +16,30 @@ func ConnectToDatabase( dbName string, logger *zerolog.Logger, ) (*SafeConn, error) { - file := fmt.Sprintf("file:%s.db", dbName) - db, err := sql.Open("sqlite", file) + 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") + 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(db, version) + err = checkDBVersion(rconn, version) if err != nil { return nil, errors.Wrap(err, "checkDBVersion") } - conn := MakeSafe(db, logger) + conn := MakeSafe(wconn, rconn, logger) return conn, nil } diff --git a/db/safeconn.go b/pkg/db/safeconn.go similarity index 73% rename from db/safeconn.go rename to pkg/db/safeconn.go index 4d66f70..0fa1560 100644 --- a/db/safeconn.go +++ b/pkg/db/safeconn.go @@ -10,7 +10,8 @@ import ( ) type SafeConn struct { - db *sql.DB + wconn *sql.DB + rconn *sql.DB readLockCount uint32 globalLockStatus uint32 globalLockRequested uint32 @@ -18,8 +19,8 @@ type SafeConn struct { } // Make the provided db handle safe and attach a logger to it -func MakeSafe(db *sql.DB, logger *zerolog.Logger) *SafeConn { - return &SafeConn{db: db, logger: logger} +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 @@ -61,7 +62,7 @@ func (conn *SafeConn) releaseReadLock() { // 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) (*SafeTX, error) { +func (conn *SafeConn) Begin(ctx context.Context) (*SafeWTX, error) { lockAcquired := make(chan struct{}) lockCtx, cancel := context.WithCancel(ctx) defer cancel() @@ -79,12 +80,44 @@ func (conn *SafeConn) Begin(ctx context.Context) (*SafeTX, error) { select { case <-lockAcquired: - tx, err := conn.db.BeginTx(ctx, nil) + tx, err := conn.wconn.BeginTx(ctx, nil) if err != nil { conn.releaseReadLock() return nil, err } - return &SafeTX{tx: tx, sc: conn}, nil + 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") @@ -125,5 +158,5 @@ func (conn *SafeConn) Close() error { conn.acquireGlobalLock() defer conn.releaseGlobalLock() conn.logger.Debug().Msg("Closing database connection") - return conn.db.Close() + return conn.wconn.Close() } diff --git a/db/safeconntx_test.go b/pkg/db/safeconntx_test.go similarity index 90% rename from db/safeconntx_test.go rename to pkg/db/safeconntx_test.go index b1816d7..35d483a 100644 --- a/db/safeconntx_test.go +++ b/pkg/db/safeconntx_test.go @@ -2,7 +2,7 @@ package db import ( "context" - "projectreshoot/tests" + "projectreshoot/pkg/tests" "strconv" "sync" "testing" @@ -18,9 +18,9 @@ func TestSafeConn(t *testing.T) { logger := tests.NilLogger() ver, err := strconv.ParseInt(cfg.DBName, 10, 0) require.NoError(t, err) - conn, err := tests.SetupTestDB(ver) + wconn, rconn, err := tests.SetupTestDB(ver) require.NoError(t, err) - sconn := MakeSafe(conn, logger) + sconn := MakeSafe(wconn, rconn, logger) defer sconn.Close() t.Run("Global lock waits for read locks to finish", func(t *testing.T) { @@ -87,9 +87,9 @@ func TestSafeTX(t *testing.T) { logger := tests.NilLogger() ver, err := strconv.ParseInt(cfg.DBName, 10, 0) require.NoError(t, err) - conn, err := tests.SetupTestDB(ver) + wconn, rconn, err := tests.SetupTestDB(ver) require.NoError(t, err) - sconn := MakeSafe(conn, logger) + sconn := MakeSafe(wconn, rconn, logger) defer sconn.Close() t.Run("Commit releases lock", func(t *testing.T) { @@ -106,12 +106,12 @@ func TestSafeTX(t *testing.T) { tx.Rollback() assert.Equal(t, uint32(0), sconn.readLockCount) }) - t.Run("Multiple TX can gain read lock", func(t *testing.T) { - tx1, err := sconn.Begin(t.Context()) + 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.Begin(t.Context()) + tx2, err := sconn.RBegin(t.Context()) require.NoError(t, err) - tx3, err := sconn.Begin(t.Context()) + tx3, err := sconn.RBegin(t.Context()) require.NoError(t, err) tx1.Commit() tx2.Commit() diff --git a/pkg/db/safetx.go b/pkg/db/safetx.go new file mode 100644 index 0000000..070b23f --- /dev/null +++ b/pkg/db/safetx.go @@ -0,0 +1,163 @@ +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 +} diff --git a/pkg/embedfs/embedfs.go b/pkg/embedfs/embedfs.go new file mode 100644 index 0000000..e730359 --- /dev/null +++ b/pkg/embedfs/embedfs.go @@ -0,0 +1,20 @@ +package embedfs + +import ( + "embed" + "io/fs" + + "github.com/pkg/errors" +) + +//go:embed files/* +var embeddedFiles embed.FS + +// Gets the embedded files +func GetEmbeddedFS() (fs.FS, error) { + subFS, err := fs.Sub(embeddedFiles, "files") + if err != nil { + return nil, errors.Wrap(err, "fs.Sub") + } + return subFS, nil +} diff --git a/pkg/embedfs/files/assets/error.png b/pkg/embedfs/files/assets/error.png new file mode 100644 index 0000000000000000000000000000000000000000..d1ffa7daa50f584ab95aecbdfa1baeab58044be0 GIT binary patch literal 21893 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 :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-4 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); + } + } + .gap-y-4 { + row-gap: calc(var(--spacing) * 4); + } + .space-x-2 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse))); + } + } + .space-x-4 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse))); + } + } + .divide-y { + :where(& > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } + } + .divide-surface2 { + :where(& > :not(:last-child)) { + border-color: var(--surface2); + } + } + .overflow-hidden { + overflow: hidden; + } + .overflow-x-hidden { + overflow-x: hidden; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .rounded-sm { + border-radius: var(--radius-sm); + } + .rounded-xl { + border-radius: var(--radius-xl); + } + .rounded-l-xl { + border-top-left-radius: var(--radius-xl); + border-bottom-left-radius: var(--radius-xl); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-e { + border-inline-end-style: var(--tw-border-style); + border-inline-end-width: 1px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-gray-200 { + border-color: var(--color-gray-200); + } + .border-overlay0 { + border-color: var(--overlay0); + } + .border-surface1 { + border-color: var(--surface1); + } + .border-surface2 { + border-color: var(--surface2); + } + .border-transparent { + border-color: transparent; + } + .bg-base { + background-color: var(--base); + } + .bg-blue { + background-color: var(--blue); + } + .bg-crust { + background-color: var(--crust); + } + .bg-dark-red { + background-color: var(--dark-red); + } + .bg-green { + background-color: var(--green); + } + .bg-mantle { + background-color: var(--mantle); + } + .bg-mauve { + background-color: var(--mauve); + } + .bg-overlay0 { + background-color: var(--overlay0); + } + .bg-overlay0\/55 { + background-color: color-mix(in oklab, var(--overlay0) 55%, transparent); + } + .bg-overlay2 { + background-color: var(--overlay2); + } + .bg-sapphire { + background-color: var(--sapphire); + } + .bg-surface0 { + background-color: var(--surface0); + } + .bg-surface2 { + background-color: var(--surface2); + } + .bg-teal { + background-color: var(--teal); + } + .object-cover { + object-fit: cover; + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-2\.5 { + padding: calc(var(--spacing) * 2.5); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-5 { + padding: calc(var(--spacing) * 5); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-5 { + padding-inline: calc(var(--spacing) * 5); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-6 { + padding-block: calc(var(--spacing) * 6); + } + .py-8 { + padding-block: calc(var(--spacing) * 8); + } + .pe-2 { + padding-inline-end: calc(var(--spacing) * 2); + } + .pe-3 { + padding-inline-end: calc(var(--spacing) * 3); + } + .pt-3 { + padding-top: calc(var(--spacing) * 3); + } + .pt-9 { + padding-top: calc(var(--spacing) * 9); + } + .pr-2 { + padding-right: calc(var(--spacing) * 2); + } + .pb-2 { + padding-bottom: calc(var(--spacing) * 2); + } + .pb-6 { + padding-bottom: calc(var(--spacing) * 6); + } + .pl-5 { + padding-left: calc(var(--spacing) * 5); + } + .text-center { + text-align: center; + } + .text-left { + text-align: left; + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-9xl { + font-size: var(--text-9xl); + line-height: var(--tw-leading, var(--text-9xl--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .leading-relaxed { + --tw-leading: var(--leading-relaxed); + line-height: var(--leading-relaxed); + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .tracking-tight { + --tw-tracking: var(--tracking-tight); + letter-spacing: var(--tracking-tight); + } + .text-blue { + color: var(--blue); + } + .text-crust { + color: var(--crust); + } + .text-mantle { + color: var(--mantle); + } + .text-overlay0 { + color: var(--overlay0); + } + .text-overlay2 { + color: var(--overlay2); + } + .text-red { + color: var(--red); + } + .text-subtext0 { + color: var(--subtext0); + } + .text-subtext1 { + color: var(--subtext1); + } + .text-text { + color: var(--text); + } + .uppercase { + text-transform: uppercase; + } + .italic { + font-style: italic; + } + .underline { + text-decoration-line: underline; + } + .decoration-2 { + text-decoration-thickness: 2px; + } + .opacity-0 { + opacity: 0%; + } + .opacity-100 { + opacity: 100%; + } + .shadow-2xl { + --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-sm { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + 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); + } + .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-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } + .ease-in-out { + --tw-ease: var(--ease-in-out); + transition-timing-function: var(--ease-in-out); + } + .before\:me-6 { + &::before { + content: var(--tw-content); + margin-inline-end: calc(var(--spacing) * 6); + } + } + .before\:flex-1 { + &::before { + content: var(--tw-content); + flex: 1; + } + } + .before\:border-t { + &::before { + content: var(--tw-content); + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + } + .before\:border-overlay1 { + &::before { + content: var(--tw-content); + border-color: var(--overlay1); + } + } + .after\:ms-6 { + &::after { + content: var(--tw-content); + margin-inline-start: calc(var(--spacing) * 6); + } + } + .after\:flex-1 { + &::after { + content: var(--tw-content); + flex: 1; + } + } + .after\:border-t { + &::after { + content: var(--tw-content); + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + } + .after\:border-overlay1 { + &::after { + content: var(--tw-content); + border-color: var(--overlay1); + } + } + .hover\:cursor-pointer { + &:hover { + @media (hover: hover) { + cursor: pointer; + } + } + } + .hover\:bg-blue\/75 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--blue) 75%, transparent); + } + } + } + .hover\:bg-crust { + &:hover { + @media (hover: hover) { + background-color: var(--crust); + } + } + } + .hover\:bg-green\/75 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--green) 75%, transparent); + } + } + } + .hover\:bg-mantle { + &:hover { + @media (hover: hover) { + background-color: var(--mantle); + } + } + } + .hover\:bg-mauve\/75 { + &:hover { + @media (hover: hover) { + 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); + } + } + } + .hover\:bg-sapphire\/75 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--sapphire) 75%, transparent); + } + } + } + .hover\:bg-surface1 { + &:hover { + @media (hover: hover) { + background-color: var(--surface1); + } + } + } + .hover\:bg-surface2 { + &:hover { + @media (hover: hover) { + background-color: var(--surface2); + } + } + } + .hover\:bg-teal\/75 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, var(--teal) 75%, transparent); + } + } + } + .hover\:text-green { + &:hover { + @media (hover: hover) { + color: var(--green); + } + } + } + .hover\:text-overlay2\/75 { + &:hover { + @media (hover: hover) { + color: color-mix(in oklab, var(--overlay2) 75%, transparent); + } + } + } + .hover\:text-subtext0 { + &:hover { + @media (hover: hover) { + color: var(--subtext0); + } + } + } + .hover\:text-subtext1 { + &:hover { + @media (hover: hover) { + color: var(--subtext1); + } + } + } + .hover\:underline { + &:hover { + @media (hover: hover) { + text-decoration-line: underline; + } + } + } + .focus\:border-blue { + &:focus { + border-color: var(--blue); + } + } + .focus\:underline { + &:focus { + text-decoration-line: underline; + } + } + .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); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .focus\:ring-blue { + &:focus { + --tw-ring-color: var(--blue); + } + } + .focus\:ring-blue-500 { + &:focus { + --tw-ring-color: var(--color-blue-500); + } + } + .focus\:outline-none { + &:focus { + --tw-outline-style: none; + outline-style: none; + } + } + .disabled\:pointer-events-none { + &:disabled { + pointer-events: none; + } + } + .disabled\:cursor-default { + &:disabled { + cursor: default; + } + } + .disabled\:bg-blue\/60 { + &:disabled { + 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); + } + } + .disabled\:opacity-50 { + &:disabled { + opacity: 50%; + } + } + .sm\:start-68 { + @media (width >= 40rem) { + inset-inline-start: calc(var(--spacing) * 68); + } + } + .sm\:end-6 { + @media (width >= 40rem) { + inset-inline-end: calc(var(--spacing) * 6); + } + } + .sm\:mt-0 { + @media (width >= 40rem) { + margin-top: calc(var(--spacing) * 0); + } + } + .sm\:ml-2 { + @media (width >= 40rem) { + margin-left: calc(var(--spacing) * 2); + } + } + .sm\:ml-5 { + @media (width >= 40rem) { + margin-left: calc(var(--spacing) * 5); + } + } + .sm\:ml-25 { + @media (width >= 40rem) { + margin-left: calc(var(--spacing) * 25); + } + } + .sm\:ml-26 { + @media (width >= 40rem) { + margin-left: calc(var(--spacing) * 26); + } + } + .sm\:ml-43 { + @media (width >= 40rem) { + margin-left: calc(var(--spacing) * 43); + } + } + .sm\:ml-45 { + @media (width >= 40rem) { + margin-left: calc(var(--spacing) * 45); + } + } + .sm\:block { + @media (width >= 40rem) { + display: block; + } + } + .sm\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .sm\:hidden { + @media (width >= 40rem) { + display: none; + } + } + .sm\:inline { + @media (width >= 40rem) { + display: inline; + } + } + .sm\:flex-row { + @media (width >= 40rem) { + flex-direction: row; + } + } + .sm\:items-center { + @media (width >= 40rem) { + align-items: center; + } + } + .sm\:justify-between { + @media (width >= 40rem) { + justify-content: space-between; + } + } + .sm\:gap-2 { + @media (width >= 40rem) { + gap: calc(var(--spacing) * 2); + } + } + .sm\:p-7 { + @media (width >= 40rem) { + padding: calc(var(--spacing) * 7); + } + } + .sm\:px-6 { + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + } + .sm\:pt-2 { + @media (width >= 40rem) { + padding-top: calc(var(--spacing) * 2); + } + } + .sm\:text-4xl { + @media (width >= 40rem) { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + } + .md\:mx-auto { + @media (width >= 48rem) { + margin-inline: auto; + } + } + .md\:mt-0 { + @media (width >= 48rem) { + margin-top: calc(var(--spacing) * 0); + } + } + .md\:ml-\[200px\] { + @media (width >= 48rem) { + margin-left: 200px; + } + } + .md\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .md\:hidden { + @media (width >= 48rem) { + display: none; + } + } + .md\:h-30 { + @media (width >= 48rem) { + height: calc(var(--spacing) * 30); + } + } + .md\:w-30 { + @media (width >= 48rem) { + width: calc(var(--spacing) * 30); + } + } + .md\:w-\[180px\] { + @media (width >= 48rem) { + width: 180px; + } + } + .md\:w-\[300px\] { + @media (width >= 48rem) { + width: 300px; + } + } + .md\:gap-8 { + @media (width >= 48rem) { + gap: calc(var(--spacing) * 8); + } + } + .md\:rounded-lg { + @media (width >= 48rem) { + border-radius: var(--radius-lg); + } + } + .md\:rounded-md { + @media (width >= 48rem) { + border-radius: var(--radius-md); + } + } + .md\:bg-surface0 { + @media (width >= 48rem) { + background-color: var(--surface0); + } + } + .md\:p-2 { + @media (width >= 48rem) { + padding: calc(var(--spacing) * 2); + } + } + .md\:px-0 { + @media (width >= 48rem) { + padding-inline: calc(var(--spacing) * 0); + } + } + .md\:px-5 { + @media (width >= 48rem) { + padding-inline: calc(var(--spacing) * 5); + } + } + .md\:pt-5 { + @media (width >= 48rem) { + padding-top: calc(var(--spacing) * 5); + } + } + .md\:text-3xl { + @media (width >= 48rem) { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + } + .md\:text-lg { + @media (width >= 48rem) { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + } + .lg\:end-8 { + @media (width >= 64rem) { + inset-inline-end: calc(var(--spacing) * 8); + } + } + .lg\:mt-0 { + @media (width >= 64rem) { + margin-top: calc(var(--spacing) * 0); + } + } + .lg\:flex { + @media (width >= 64rem) { + display: flex; + } + } + .lg\:inline { + @media (width >= 64rem) { + display: inline; + } + } + .lg\:items-end { + @media (width >= 64rem) { + align-items: flex-end; + } + } + .lg\:justify-between { + @media (width >= 64rem) { + justify-content: space-between; + } + } + .lg\:justify-end { + @media (width >= 64rem) { + justify-content: flex-end; + } + } + .lg\:justify-start { + @media (width >= 64rem) { + justify-content: flex-start; + } + } + .lg\:gap-12 { + @media (width >= 64rem) { + gap: calc(var(--spacing) * 12); + } + } + .lg\:px-8 { + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .lg\:text-6xl { + @media (width >= 64rem) { + font-size: var(--text-6xl); + line-height: var(--tw-leading, var(--text-6xl--line-height)); + } + } +} +[x-cloak] { + display: none !important; +} +:root { + --rosewater: hsl(11, 59%, 67%); + --flamingo: hsl(0, 60%, 67%); + --pink: hsl(316, 73%, 69%); + --mauve: hsl(266, 85%, 58%); + --red: hsl(347, 87%, 44%); + --dark-red: hsl(343, 50%, 82%); + --maroon: hsl(355, 76%, 59%); + --peach: hsl(22, 99%, 52%); + --yellow: hsl(35, 77%, 49%); + --green: hsl(109, 58%, 40%); + --teal: hsl(183, 74%, 35%); + --sky: hsl(197, 97%, 46%); + --sapphire: hsl(189, 70%, 42%); + --blue: hsl(220, 91%, 54%); + --lavender: hsl(231, 97%, 72%); + --text: hsl(234, 16%, 35%); + --subtext1: hsl(233, 13%, 41%); + --subtext0: hsl(233, 10%, 47%); + --overlay2: hsl(232, 10%, 53%); + --overlay1: hsl(231, 10%, 59%); + --overlay0: hsl(228, 11%, 65%); + --surface2: hsl(227, 12%, 71%); + --surface1: hsl(225, 14%, 77%); + --surface0: hsl(223, 16%, 83%); + --base: hsl(220, 23%, 95%); + --mantle: hsl(220, 22%, 92%); + --crust: hsl(220, 21%, 89%); +} +.dark { + --rosewater: hsl(10, 56%, 91%); + --flamingo: hsl(0, 59%, 88%); + --pink: hsl(316, 72%, 86%); + --mauve: hsl(267, 84%, 81%); + --red: hsl(343, 81%, 75%); + --dark-red: hsl(316, 19%, 27%); + --maroon: hsl(350, 65%, 77%); + --peach: hsl(23, 92%, 75%); + --yellow: hsl(41, 86%, 83%); + --green: hsl(115, 54%, 76%); + --teal: hsl(170, 57%, 73%); + --sky: hsl(189, 71%, 73%); + --sapphire: hsl(199, 76%, 69%); + --blue: hsl(217, 92%, 76%); + --lavender: hsl(232, 97%, 85%); + --text: hsl(226, 64%, 88%); + --subtext1: hsl(227, 35%, 80%); + --subtext0: hsl(228, 24%, 72%); + --overlay2: hsl(228, 17%, 64%); + --overlay1: hsl(230, 13%, 55%); + --overlay0: hsl(231, 11%, 47%); + --surface2: hsl(233, 12%, 39%); + --surface1: hsl(234, 13%, 31%); + --surface0: hsl(237, 16%, 23%); + --base: hsl(240, 21%, 15%); + --mantle: hsl(240, 21%, 12%); + --crust: hsl(240, 23%, 9%); +} +.ubuntu-mono-regular { + font-family: "Ubuntu Mono", serif; + font-weight: 400; + font-style: normal; +} +.ubuntu-mono-bold { + font-family: "Ubuntu Mono", serif; + font-weight: 700; + font-style: normal; +} +.ubuntu-mono-regular-italic { + font-family: "Ubuntu Mono", serif; + font-weight: 400; + font-style: italic; +} +.ubuntu-mono-bold-italic { + font-family: "Ubuntu Mono", serif; + 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; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@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: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-leading { + syntax: "*"; + inherits: false; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@property --tw-ease { + syntax: "*"; + inherits: false; +} +@property --tw-content { + syntax: "*"; + initial-value: ""; + inherits: false; +} diff --git a/static/favicon.ico b/pkg/embedfs/files/favicon.ico similarity index 100% rename from static/favicon.ico rename to pkg/embedfs/files/favicon.ico diff --git a/jwt/create.go b/pkg/jwt/create.go similarity index 94% rename from jwt/create.go rename to pkg/jwt/create.go index 617abce..1ba66a4 100644 --- a/jwt/create.go +++ b/pkg/jwt/create.go @@ -3,8 +3,8 @@ package jwt import ( "time" - "projectreshoot/config" - "projectreshoot/db" + "projectreshoot/internal/models" + "projectreshoot/pkg/config" "github.com/golang-jwt/jwt" "github.com/google/uuid" @@ -14,7 +14,7 @@ import ( // Generates an access token for the provided user func GenerateAccessToken( config *config.Config, - user *db.User, + user *models.User, fresh bool, rememberMe bool, ) (tokenStr string, exp int64, err error) { @@ -54,7 +54,7 @@ func GenerateAccessToken( // Generates a refresh token for the provided user func GenerateRefreshToken( config *config.Config, - user *db.User, + user *models.User, rememberMe bool, ) (tokenStr string, exp int64, err error) { issuedAt := time.Now().Unix() diff --git a/jwt/parse.go b/pkg/jwt/parse.go similarity index 98% rename from jwt/parse.go rename to pkg/jwt/parse.go index 0446e85..fb56778 100644 --- a/jwt/parse.go +++ b/pkg/jwt/parse.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - "projectreshoot/config" - "projectreshoot/db" + "projectreshoot/pkg/config" + "projectreshoot/pkg/db" "github.com/golang-jwt/jwt" "github.com/google/uuid" @@ -19,7 +19,7 @@ import ( func ParseAccessToken( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx db.SafeTX, tokenString string, ) (*AccessToken, error) { if tokenString == "" { @@ -92,7 +92,7 @@ func ParseAccessToken( func ParseRefreshToken( config *config.Config, ctx context.Context, - tx *db.SafeTX, + tx db.SafeTX, tokenString string, ) (*RefreshToken, error) { if tokenString == "" { diff --git a/jwt/revoke.go b/pkg/jwt/revoke.go similarity index 77% rename from jwt/revoke.go rename to pkg/jwt/revoke.go index 016f33e..64f4020 100644 --- a/jwt/revoke.go +++ b/pkg/jwt/revoke.go @@ -2,13 +2,13 @@ package jwt import ( "context" - "projectreshoot/db" + "projectreshoot/pkg/db" "github.com/pkg/errors" ) // Revoke a token by adding it to the database -func RevokeToken(ctx context.Context, tx *db.SafeTX, t Token) error { +func RevokeToken(ctx context.Context, tx *db.SafeWTX, t Token) error { jti := t.GetJTI() exp := t.GetEXP() query := `INSERT INTO jwtblacklist (jti, exp) VALUES (?, ?)` @@ -20,7 +20,7 @@ func RevokeToken(ctx context.Context, tx *db.SafeTX, t Token) error { } // Check if a token has been revoked. Returns true if not revoked. -func CheckTokenNotRevoked(ctx context.Context, tx *db.SafeTX, t Token) (bool, error) { +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) diff --git a/jwt/tokens.go b/pkg/jwt/tokens.go similarity index 79% rename from jwt/tokens.go rename to pkg/jwt/tokens.go index ae5d97a..26bdcc8 100644 --- a/jwt/tokens.go +++ b/pkg/jwt/tokens.go @@ -2,7 +2,8 @@ package jwt import ( "context" - "projectreshoot/db" + "projectreshoot/internal/models" + "projectreshoot/pkg/db" "github.com/google/uuid" "github.com/pkg/errors" @@ -12,7 +13,7 @@ type Token interface { GetJTI() uuid.UUID GetEXP() int64 GetScope() string - GetUser(ctx context.Context, tx *db.SafeTX) (*db.User, error) + GetUser(ctx context.Context, tx db.SafeTX) (*models.User, error) } // Access token @@ -38,15 +39,15 @@ type RefreshToken struct { Scope string // Should be "refresh" } -func (a AccessToken) GetUser(ctx context.Context, tx *db.SafeTX) (*db.User, error) { - user, err := db.GetUserFromID(ctx, tx, a.SUB) +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) (*db.User, error) { - user, err := db.GetUserFromID(ctx, tx, r.SUB) +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") } diff --git a/logging/logger.go b/pkg/logging/logger.go similarity index 100% rename from logging/logger.go rename to pkg/logging/logger.go diff --git a/tests/config.go b/pkg/tests/config.go similarity index 91% rename from tests/config.go rename to pkg/tests/config.go index 28f53d5..757d3b0 100644 --- a/tests/config.go +++ b/pkg/tests/config.go @@ -2,7 +2,7 @@ package tests import ( "os" - "projectreshoot/config" + "projectreshoot/pkg/config" "github.com/pkg/errors" ) diff --git a/pkg/tests/database.go b/pkg/tests/database.go new file mode 100644 index 0000000..d5856a6 --- /dev/null +++ b/pkg/tests/database.go @@ -0,0 +1,116 @@ +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 +} diff --git a/tests/logger.go b/pkg/tests/logger.go similarity index 100% rename from tests/logger.go rename to pkg/tests/logger.go diff --git a/tests/testdata.sql b/pkg/tests/testdata.sql similarity index 100% rename from tests/testdata.sql rename to pkg/tests/testdata.sql diff --git a/tmdb/config.go b/pkg/tmdb/config.go similarity index 100% rename from tmdb/config.go rename to pkg/tmdb/config.go diff --git a/tmdb/credits.go b/pkg/tmdb/credits.go similarity index 100% rename from tmdb/credits.go rename to pkg/tmdb/credits.go diff --git a/tmdb/crew_functions.go b/pkg/tmdb/crew_functions.go similarity index 100% rename from tmdb/crew_functions.go rename to pkg/tmdb/crew_functions.go diff --git a/tmdb/movie.go b/pkg/tmdb/movie.go similarity index 100% rename from tmdb/movie.go rename to pkg/tmdb/movie.go diff --git a/tmdb/movie_functions.go b/pkg/tmdb/movie_functions.go similarity index 100% rename from tmdb/movie_functions.go rename to pkg/tmdb/movie_functions.go diff --git a/tmdb/request.go b/pkg/tmdb/request.go similarity index 100% rename from tmdb/request.go rename to pkg/tmdb/request.go diff --git a/tmdb/search.go b/pkg/tmdb/search.go similarity index 100% rename from tmdb/search.go rename to pkg/tmdb/search.go diff --git a/tmdb/structs.go b/pkg/tmdb/structs.go similarity index 94% rename from tmdb/structs.go rename to pkg/tmdb/structs.go index 0929dbd..3a68d08 100644 --- a/tmdb/structs.go +++ b/pkg/tmdb/structs.go @@ -1,9 +1,5 @@ package tmdb -import ( -// "encoding/json" -) - type Genre struct { ID int `json:"id"` Name string `json:"name"` diff --git a/tests/database.go b/tests/database.go deleted file mode 100644 index db417f9..0000000 --- a/tests/database.go +++ /dev/null @@ -1,90 +0,0 @@ -package tests - -import ( - "context" - "database/sql" - "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, "main.go")); err == nil { - migrationsdir := os.DirFS(filepath.Join(dir, "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, "main.go")); err == nil { - return filepath.Join(dir, "tests", "testdata.sql"), nil - } - - parent := filepath.Dir(dir) - if parent == dir { // Reached root - return "", errors.New("Unable to locate test data") - } - dir = parent - } -} - -func SetupTestDB(version int64) (*sql.DB, error) { - conn, err := sql.Open("sqlite", "file::memory:?cache=shared") - if err != nil { - return nil, errors.Wrap(err, "sql.Open") - } - - migrations, err := findMigrations() - if err != nil { - return nil, errors.Wrap(err, "findMigrations") - } - provider, err := goose.NewProvider(goose.DialectSQLite3, conn, *migrations) - if err != nil { - return nil, errors.Wrap(err, "goose.NewProvider") - } - ctx := context.Background() - if _, err := provider.UpTo(ctx, version); err != nil { - return nil, errors.Wrap(err, "provider.UpTo") - } - - // Load the test data - dataPath, err := findTestData() - if err != nil { - return nil, errors.Wrap(err, "findSchema") - } - sqlBytes, err := os.ReadFile(dataPath) - if err != nil { - return nil, errors.Wrap(err, "os.ReadFile") - } - dataSQL := string(sqlBytes) - - _, err = conn.Exec(dataSQL) - if err != nil { - return nil, errors.Wrap(err, "tx.Exec") - } - return conn, nil -}