I'm done
Some checks failed
Build / build (push) Has been cancelled

This commit is contained in:
Gustavo Maronato 2023-08-24 22:03:58 -03:00
parent 16bb373c60
commit 949ea57dd9
Signed by: maronato
SSH Key Fingerprint: SHA256:2Gw7kwMz/As+2UkR1qQ/qYYhn+WNh3FGv6ozhoRrLcs
49 changed files with 1179 additions and 502 deletions

View File

@ -2,6 +2,7 @@
*.db *.db
*.db-shm *.db-shm
*.db-wal *.db-wal
results.bin
# ---> Go # ---> Go
# If you prefer the allow list template instead of the deny list, see community template: # If you prefer the allow list template instead of the deny list, see community template:

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
*.db *.db
*.db-shm *.db-shm
*.db-wal *.db-wal
results.bin
# ---> Go # ---> Go
# If you prefer the allow list template instead of the deny list, see community template: # If you prefer the allow list template instead of the deny list, see community template:

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
golang 1.21.0

View File

@ -1,5 +1,5 @@
# Load the golang image # Load the golang image
FROM golang:1.20.7 as go-builder FROM golang:1.21.0 as go-builder
# Then the node image # Then the node image
FROM node:20.5.0 as builder FROM node:20.5.0 as builder
@ -15,7 +15,6 @@ WORKDIR /go/src/app
ENV GOPATH=/go ENV GOPATH=/go
ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH
ENV GOCACHE=/tmp/.go-build-cache ENV GOCACHE=/tmp/.go-build-cache
ENV CGO_ENABLED=0
# This variable communicates to the service that it's running inside # This variable communicates to the service that it's running inside
# a docker container. # a docker container.
ENV ENV_DOCKER=true ENV ENV_DOCKER=true
@ -51,10 +50,13 @@ WORKDIR /app
# Set our runtime environment # Set our runtime environment
ENV ENV_DOCKER=true ENV ENV_DOCKER=true
ENV GOSHORT_PROD=true
COPY --from=builder /go/src/app/goshort /usr/local/bin/goshort COPY --from=builder /go/src/app/goshort /usr/local/bin/goshort
HEALTHCHECK CMD [ "goshort", "healthcheck" ] HEALTHCHECK CMD [ "goshort", "healthcheck" ]
EXPOSE 8080
ENTRYPOINT [ "goshort" ] ENTRYPOINT [ "goshort" ]
CMD [ "serve" ] CMD [ "serve" ]

View File

@ -7,7 +7,7 @@ frontend:
VITE_API_URL=/api npm run --prefix frontend build VITE_API_URL=/api npm run --prefix frontend build
backend: backend:
CGO_ENABLED=0 go build -o goshort goshort.go go build -o goshort goshort.go
all: all:
make frontend make frontend

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/http" "net/http"
@ -16,9 +17,11 @@ import (
servermiddleware "git.maronato.dev/maronato/goshort/internal/server/middleware" servermiddleware "git.maronato.dev/maronato/goshort/internal/server/middleware"
shortserver "git.maronato.dev/maronato/goshort/internal/server/short" shortserver "git.maronato.dev/maronato/goshort/internal/server/short"
shortservice "git.maronato.dev/maronato/goshort/internal/service/short" shortservice "git.maronato.dev/maronato/goshort/internal/service/short"
shortlogservice "git.maronato.dev/maronato/goshort/internal/service/shortlog"
tokenservice "git.maronato.dev/maronato/goshort/internal/service/token" tokenservice "git.maronato.dev/maronato/goshort/internal/service/token"
userservice "git.maronato.dev/maronato/goshort/internal/service/user" userservice "git.maronato.dev/maronato/goshort/internal/service/user"
handlerutils "git.maronato.dev/maronato/goshort/internal/util/handler" handlerutils "git.maronato.dev/maronato/goshort/internal/util/handler"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
@ -50,6 +53,9 @@ func New(cfg *config.Config) *ffcli.Command {
} }
func exec(ctx context.Context, cfg *config.Config) error { func exec(ctx context.Context, cfg *config.Config) error {
l := logging.FromCtx(ctx)
l.Debug("Executing dev command", slog.Any("config", cfg))
eg, egCtx := errgroup.WithContext(ctx) eg, egCtx := errgroup.WithContext(ctx)
// Start the API server // Start the API server
@ -84,11 +90,16 @@ func serveAPI(ctx context.Context, cfg *config.Config) error {
shortService := shortservice.NewShortService(storage) shortService := shortservice.NewShortService(storage)
userService := userservice.NewUserService(cfg, storage) userService := userservice.NewUserService(cfg, storage)
tokenService := tokenservice.NewTokenService(storage) tokenService := tokenservice.NewTokenService(storage)
shortLogService := shortlogservice.NewShortLogService(storage)
// Start short log worker
stopWorker := shortLogService.StartWorker(ctx)
defer stopWorker()
// Create handlers // Create handlers
apiHandler := apiserver.NewAPIHandler(shortService, userService, tokenService) apiHandler := apiserver.NewAPIHandler(shortService, userService, tokenService, shortLogService)
shortHandler := shortserver.NewShortHandler(shortService) shortHandler := shortserver.NewShortHandler(shortService, shortLogService)
healthcheckHandler := healthcheckserver.NewHealthcheckHandler() healthcheckHandler := healthcheckserver.NewHealthcheckHandler(storage)
// Create routers // Create routers
apiRouter := apiserver.NewAPIRouter(apiHandler) apiRouter := apiserver.NewAPIRouter(apiHandler)
@ -105,7 +116,7 @@ func serveAPI(ctx context.Context, cfg *config.Config) error {
AllowedOrigins: []string{ AllowedOrigins: []string{
"http://" + net.JoinHostPort(cfg.Host, cfg.UIPort), "http://" + net.JoinHostPort(cfg.Host, cfg.UIPort),
}, },
AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"}, AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE", "OPTIONS"},
AllowCredentials: true, AllowCredentials: true,
})) }))

View File

@ -4,12 +4,14 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"git.maronato.dev/maronato/goshort/cmd/shared" "git.maronato.dev/maronato/goshort/cmd/shared"
"git.maronato.dev/maronato/goshort/internal/config" "git.maronato.dev/maronato/goshort/internal/config"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
) )
@ -38,6 +40,9 @@ func New(cfg *config.Config) *ffcli.Command {
// If the request fails, return an error // If the request fails, return an error
// Otherwise, return nil. // Otherwise, return nil.
func exec(ctx context.Context, cfg *config.Config) error { func exec(ctx context.Context, cfg *config.Config) error {
l := logging.FromCtx(ctx)
l.Debug("Executing healthcheck command", slog.Any("config", cfg))
addr := url.URL{ addr := url.URL{
Host: net.JoinHostPort(cfg.Host, cfg.Port), Host: net.JoinHostPort(cfg.Host, cfg.Port),
Scheme: "http", Scheme: "http",

View File

@ -12,6 +12,7 @@ import (
healthcheckcmd "git.maronato.dev/maronato/goshort/cmd/healthcheck" healthcheckcmd "git.maronato.dev/maronato/goshort/cmd/healthcheck"
servecmd "git.maronato.dev/maronato/goshort/cmd/serve" servecmd "git.maronato.dev/maronato/goshort/cmd/serve"
"git.maronato.dev/maronato/goshort/internal/config" "git.maronato.dev/maronato/goshort/internal/config"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
) )
@ -29,6 +30,7 @@ func Run() {
} }
// Look for the env ENV_DOCKER=true to disable the dev command // Look for the env ENV_DOCKER=true to disable the dev command
// since the docker image won't have node installed.
if os.Getenv("ENV_DOCKER") != "true" { if os.Getenv("ENV_DOCKER") != "true" {
rootCmd.Subcommands = append(rootCmd.Subcommands, devcmd.New(cfg)) rootCmd.Subcommands = append(rootCmd.Subcommands, devcmd.New(cfg))
} }
@ -45,6 +47,10 @@ func Run() {
os.Exit(1) os.Exit(1)
} }
// Create system logger
l := logging.NewLogger(cfg)
ctx = logging.WithLogger(ctx, l)
// Run the command. // Run the command.
if err := rootCmd.Run(ctx); err != nil { if err := rootCmd.Run(ctx); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"log/slog"
"git.maronato.dev/maronato/goshort/cmd/shared" "git.maronato.dev/maronato/goshort/cmd/shared"
"git.maronato.dev/maronato/goshort/internal/config" "git.maronato.dev/maronato/goshort/internal/config"
@ -14,9 +15,11 @@ import (
shortserver "git.maronato.dev/maronato/goshort/internal/server/short" shortserver "git.maronato.dev/maronato/goshort/internal/server/short"
staticssterver "git.maronato.dev/maronato/goshort/internal/server/static" staticssterver "git.maronato.dev/maronato/goshort/internal/server/static"
shortservice "git.maronato.dev/maronato/goshort/internal/service/short" shortservice "git.maronato.dev/maronato/goshort/internal/service/short"
shortlogservice "git.maronato.dev/maronato/goshort/internal/service/shortlog"
tokenservice "git.maronato.dev/maronato/goshort/internal/service/token" tokenservice "git.maronato.dev/maronato/goshort/internal/service/token"
userservice "git.maronato.dev/maronato/goshort/internal/service/user" userservice "git.maronato.dev/maronato/goshort/internal/service/user"
handlerutils "git.maronato.dev/maronato/goshort/internal/util/handler" handlerutils "git.maronato.dev/maronato/goshort/internal/util/handler"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
) )
@ -27,6 +30,7 @@ func New(cfg *config.Config) *ffcli.Command {
shared.RegisterBaseFlags(fs, cfg) shared.RegisterBaseFlags(fs, cfg)
shared.RegisterServerFlags(fs, cfg) shared.RegisterServerFlags(fs, cfg)
fs.BoolVar(&cfg.Prod, "prod", config.DefaultProd, "run in production mode")
// Create the command and options // Create the command and options
cmd := shared.NewCommand(cfg, exec) cmd := shared.NewCommand(cfg, exec)
@ -44,6 +48,9 @@ func New(cfg *config.Config) *ffcli.Command {
} }
func exec(ctx context.Context, cfg *config.Config) error { func exec(ctx context.Context, cfg *config.Config) error {
l := logging.FromCtx(ctx)
l.Debug("Executing serve command", slog.Any("config", cfg))
// Create the new server // Create the new server
server := server.NewServer(cfg) server := server.NewServer(cfg)
@ -58,12 +65,17 @@ func exec(ctx context.Context, cfg *config.Config) error {
shortService := shortservice.NewShortService(storage) shortService := shortservice.NewShortService(storage)
userService := userservice.NewUserService(cfg, storage) userService := userservice.NewUserService(cfg, storage)
tokenService := tokenservice.NewTokenService(storage) tokenService := tokenservice.NewTokenService(storage)
shortLogService := shortlogservice.NewShortLogService(storage)
// Start short log worker
stopWorker := shortLogService.StartWorker(ctx)
defer stopWorker()
// Create handlers // Create handlers
apiHandler := apiserver.NewAPIHandler(shortService, userService, tokenService) apiHandler := apiserver.NewAPIHandler(shortService, userService, tokenService, shortLogService)
shortHandler := shortserver.NewShortHandler(shortService) shortHandler := shortserver.NewShortHandler(shortService, shortLogService)
staticHandler := staticssterver.NewStaticHandler(cfg) staticHandler := staticssterver.NewStaticHandler(cfg)
healthcheckHandler := healthcheckserver.NewHealthcheckHandler() healthcheckHandler := healthcheckserver.NewHealthcheckHandler(storage)
// Create routers // Create routers
apiRouter := apiserver.NewAPIRouter(apiHandler) apiRouter := apiserver.NewAPIRouter(apiHandler)

View File

@ -41,6 +41,7 @@ func RegisterBaseFlags(fs *flag.FlagSet, cfg *config.Config) {
fs.BoolVar(&cfg.Debug, "debug", config.DefaultDebug, "enable debug mode") fs.BoolVar(&cfg.Debug, "debug", config.DefaultDebug, "enable debug mode")
var defaultHost = config.DefaultHost var defaultHost = config.DefaultHost
if os.Getenv("ENV_DOCKER") == "true" { if os.Getenv("ENV_DOCKER") == "true" {
// This is a QOL hack to allow docker to bind the port without manually specifying the host
defaultHost = "0.0.0.0" defaultHost = "0.0.0.0"
} }
fs.StringVar(&cfg.Host, "host", defaultHost, "host to listen on") fs.StringVar(&cfg.Host, "host", defaultHost, "host to listen on")
@ -59,7 +60,6 @@ func RegisterServerFlags(fs *flag.FlagSet, cfg *config.Config) {
func InitStorage(cfg *config.Config) storage.Storage { func InitStorage(cfg *config.Config) storage.Storage {
switch cfg.DBType { switch cfg.DBType {
case config.DBTypeMemory: case config.DBTypeMemory:
cfg.DBURL = ":memory:"
return sqlitestorage.NewSQLiteStorage(cfg) return sqlitestorage.NewSQLiteStorage(cfg)
case config.DBTypeSQLite: case config.DBTypeSQLite:
return sqlitestorage.NewSQLiteStorage(cfg) return sqlitestorage.NewSQLiteStorage(cfg)

View File

@ -14,12 +14,13 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"react-use": "^17.4.0" "ua-parser-js": "^1.0.35"
}, },
"devDependencies": { "devDependencies": {
"@marolint/eslint-config-react": "^1.0.2", "@marolint/eslint-config-react": "^1.0.2",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/ua-parser-js": "^0.7.36",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"eslint": "^8.47.0", "eslint": "^8.47.0",
@ -55,6 +56,7 @@
"version": "7.22.10", "version": "7.22.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz",
"integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==",
"dev": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -856,11 +858,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
"integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA=="
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.12", "version": "7.0.12",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
@ -911,6 +908,12 @@
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
"dev": true "dev": true
}, },
"node_modules/@types/ua-parser-js": {
"version": "0.7.36",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz",
"integrity": "sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.62.0", "version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
@ -1133,11 +1136,6 @@
"vite": "^4" "vite": "^4"
} }
}, },
"node_modules/@xobotyi/scrollbar-width": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.10.0", "version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
@ -1668,14 +1666,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true "dev": true
}, },
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -1690,26 +1680,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/css-in-js-utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
"dependencies": {
"hyphenate-style-name": "^1.0.3"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -1725,7 +1695,8 @@
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
"dev": true
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
@ -1829,14 +1800,6 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true "dev": true
}, },
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.22.1", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz",
@ -2434,7 +2397,8 @@
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
}, },
"node_modules/fast-diff": { "node_modules/fast-diff": {
"version": "1.3.0", "version": "1.3.0",
@ -2482,21 +2446,6 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true "dev": true
}, },
"node_modules/fast-loops": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz",
"integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g=="
},
"node_modules/fast-shallow-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz",
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
},
"node_modules/fastest-stable-stringify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz",
"integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q=="
},
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
@ -2852,11 +2801,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.2.4", "version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@ -2907,15 +2851,6 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true "dev": true
}, },
"node_modules/inline-style-prefixer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz",
"integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==",
"dependencies": {
"css-in-js-utils": "^3.1.0",
"fast-loops": "^1.1.3"
}
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
@ -3290,11 +3225,6 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -3438,11 +3368,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
},
"node_modules/merge2": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -3503,25 +3428,6 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nano-css": {
"version": "5.3.5",
"resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.3.5.tgz",
"integrity": "sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==",
"dependencies": {
"css-tree": "^1.1.2",
"csstype": "^3.0.6",
"fastest-stable-stringify": "^2.0.2",
"inline-style-prefixer": "^6.0.0",
"rtl-css-js": "^1.14.0",
"sourcemap-codec": "^1.4.8",
"stacktrace-js": "^2.0.2",
"stylis": "^4.0.6"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.6", "version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
@ -4115,45 +4021,6 @@
"react-dom": ">=16.8" "react-dom": ">=16.8"
} }
}, },
"node_modules/react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
"integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==",
"peerDependencies": {
"react": "*",
"tslib": "*"
}
},
"node_modules/react-use": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.4.0.tgz",
"integrity": "sha512-TgbNTCA33Wl7xzIJegn1HndB4qTS9u03QUwyNycUnXaweZkE4Kq2SB+Yoxx8qbshkZGYBDvUXbXWRUmQDcZZ/Q==",
"dependencies": {
"@types/js-cookie": "^2.2.6",
"@xobotyi/scrollbar-width": "^1.9.5",
"copy-to-clipboard": "^3.3.1",
"fast-deep-equal": "^3.1.3",
"fast-shallow-equal": "^1.0.0",
"js-cookie": "^2.2.1",
"nano-css": "^5.3.1",
"react-universal-interface": "^0.6.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.1.0",
"set-harmonic-interval": "^1.0.1",
"throttle-debounce": "^3.0.1",
"ts-easing": "^0.2.0",
"tslib": "^2.1.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-use/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -4198,7 +4065,8 @@
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.0", "version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
"dev": true
}, },
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.0", "version": "1.5.0",
@ -4217,11 +4085,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.4", "version": "1.22.4",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
@ -4289,14 +4152,6 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rtl-css-js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
"integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==",
"dependencies": {
"@babel/runtime": "^7.1.2"
}
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -4360,17 +4215,6 @@
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
}, },
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.5.4", "version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@ -4386,14 +4230,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/set-harmonic-interval": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz",
"integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==",
"engines": {
"node": ">=6.9"
}
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -4438,14 +4274,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@ -4455,52 +4283,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead"
},
"node_modules/stack-generator": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
"integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"node_modules/stacktrace-gps": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz",
"integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==",
"dependencies": {
"source-map": "0.5.6",
"stackframe": "^1.3.4"
}
},
"node_modules/stacktrace-gps/node_modules/source-map": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stacktrace-js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
"integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
"dependencies": {
"error-stack-parser": "^2.0.6",
"stack-generator": "^2.0.5",
"stacktrace-gps": "^3.0.4"
}
},
"node_modules/string.prototype.matchall": { "node_modules/string.prototype.matchall": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz",
@ -4598,11 +4380,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/stylis": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz",
"integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ=="
},
"node_modules/sucrase": { "node_modules/sucrase": {
"version": "3.34.0", "version": "3.34.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
@ -4733,14 +4510,6 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/throttle-debounce": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
"integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==",
"engines": {
"node": ">=10"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -4753,16 +4522,6 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/ts-easing": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz",
"integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ=="
},
"node_modules/ts-interface-checker": { "node_modules/ts-interface-checker": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -4784,7 +4543,8 @@
"node_modules/tslib": { "node_modules/tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
}, },
"node_modules/tsutils": { "node_modules/tsutils": {
"version": "3.21.0", "version": "3.21.0",
@ -4903,6 +4663,24 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/ua-parser-js": {
"version": "1.0.35",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz",
"integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",

View File

@ -16,12 +16,13 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.15.0", "react-router-dom": "^6.15.0",
"react-use": "^17.4.0" "ua-parser-js": "^1.0.35"
}, },
"devDependencies": { "devDependencies": {
"@marolint/eslint-config-react": "^1.0.2", "@marolint/eslint-config-react": "^1.0.2",
"@types/react": "^18.2.15", "@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/ua-parser-js": "^0.7.36",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"eslint": "^8.47.0", "eslint": "^8.47.0",

View File

@ -0,0 +1,153 @@
ancient
antique
aquatic
baby
basic
big
bitter
black
blue
bottle
bottled
brave
breezy
bright
brown
calm
charming
cheerful
chummy
classy
clear
clever
cloudy
cold
cool
crispy
curly
daily
deep
delightful
dizzy
down
dynamic
elated
elegant
excited
exotic
fancy
fast
fearless
festive
fluffy
fragile
fresh
friendly
funny
fuzzy
gentle
gifted
gigantic
graceful
grand
grateful
great
green
happy
heavy
helpful
hot
hungry
husky
icy
imaginary
invisible
jagged
jolly
joyful
joyous
kind
large
light
little
lively
lovely
lucky
lumpy
magical
manic
melodic
mighty
misty
modern
narrow
new
nifty
noisy
normal
odd
old
orange
ordinary
painless
pastel
peaceful
perfect
phobic
pink
polite
precious
pretty
purple
quaint
quick
quiet
rapid
red
rocky
rough
round
royal
rugged
rustic
safe
sandy
shiny
silent
silky
silly
slender
slow
small
smiling
smooth
snug
soft
sour
strange
strong
sunny
sweet
swift
thirsty
thoughtful
tiny
uneven
unusual
vanilla
vast
violet
warm
watery
weak
white
wide
wild
wilde
windy
wise
witty
wonderful
yellow
young
zany

136
frontend/public/nouns.txt Normal file
View File

@ -0,0 +1,136 @@
airplane
apple
automobile
ball
balloon
banana
beach
bird
boat
boot
bottle
box
breeze
bug
bush
butter
canoe
carrot
cartoon
cello
chair
cheese
coast
coconut
comet
cream
curtain
daisy
desk
diamond
door
earth
elephant
emerald
fire
flamingo
flower
flute
forest
free
giant
giraffe
glove
grape
grasshopper
hair
hat
hill
house
ink
iris
jade
jungle
kangaroo
kayak
lake
lemon
lightning
lion
lotus
lump
mango
mint
monkey
moon
motorcycle
mountain
nest
oboe
ocean
octopus
onion
orange
orchestra
owl
path
penguin
phoenix
piano
pineapple
planet
pond
potato
prairie
quail
rabbit
raccoon
raid
rain
raven
river
road
rosebud
ruby
sea
ship
shoe
shore
shrub
sitter
skates
sky
socks
sparrow
spider
squash
squirrel
star
stream
street
sun
table
teapot
terrain
tiger
toast
tomato
trail
train
tree
truck
trumpet
tuba
tulip
umbrella
unicorn
unit
valley
vase
violet
violin
water
wind
window
zebra
zoo

View File

@ -10,7 +10,6 @@ const ItemList = <T extends Record<string, unknown>, K extends keyof T>({
idKey: K idKey: K
Item: FunctionComponent<T> Item: FunctionComponent<T>
}) => { }) => {
console.log("list")
return ( return (
<> <>
<ul <ul

View File

@ -127,7 +127,7 @@ export default function Navbar() {
isActive isActive
? "bg-blue-100 text-blue-600 border-blue-500" ? "bg-blue-100 text-blue-600 border-blue-500"
: "text-slate-500 hover:bg-slate-50 hover:text-slate-600 hover:border-slate-500", : "text-slate-500 hover:bg-slate-50 hover:text-slate-600 hover:border-slate-500",
"block border-l-4 border-transparent px-3 py-2 text-base font-medium" "block border-l-4 px-3 py-2 text-base font-medium"
) )
}> }>
{item.name} {item.name}

View File

@ -1,11 +0,0 @@
import { FunctionComponent } from "react"
const NoItems: FunctionComponent<{ type: string }> = ({ type }) => {
return (
<div className="text-center pt-5 text-xl font-light">
{`No ${type}s yet`}
</div>
)
}
export default NoItems

View File

@ -1,12 +1,36 @@
import { FunctionComponent, useCallback } from "react" import { FunctionComponent, useCallback, useMemo } from "react"
import {
ComputerDesktopIcon,
DevicePhoneMobileIcon,
DeviceTabletIcon,
TvIcon,
} from "@heroicons/react/24/outline"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { UAParser } from "ua-parser-js"
import { useDelete } from "../hooks/useCRUD" import { useDelete } from "../hooks/useCRUD"
import { Session } from "../types" import { Session } from "../types"
import { formatDateStr } from "../util/date"
import ItemBase from "./ItemBase" import ItemBase from "./ItemBase"
const UAIcon: FunctionComponent<{ device?: string; className?: string }> = ({
device,
className,
}) => {
switch (device) {
case "mobile":
return <DevicePhoneMobileIcon className={className} />
case "tablet":
return <DeviceTabletIcon className={className} />
case "smarttv":
return <TvIcon className={className} />
default:
return <ComputerDesktopIcon className={className} />
}
}
const SessionItem: FunctionComponent<Session> = ({ ...session }) => { const SessionItem: FunctionComponent<Session> = ({ ...session }) => {
// Handle deletion // Handle deletion
const [deleting, deleteOther] = useDelete() const [deleting, deleteOther] = useDelete()
@ -21,9 +45,54 @@ const SessionItem: FunctionComponent<Session> = ({ ...session }) => {
} }
}, [deleteCurrent, deleteOther, session]) }, [deleteCurrent, deleteOther, session])
const ua = useMemo(() => {
const ua = new UAParser(session.userAgent)
return {
device: ua.getDevice().type,
browser: ua.getBrowser().name,
os: ua.getOS().name,
osVersion: ua.getOS().version,
}
}, [session.userAgent])
const lastActivity = formatDateStr(session.lastActivity)
return ( return (
<ItemBase doDelete={doDelete} deleting={deleting}> <ItemBase doDelete={doDelete} deleting={deleting}>
{JSON.stringify(session)} <div className="grid grid-cols-12 gap-y-4 mb-2">
<div className="col-span-8 order-1 sm:col-span-4 flex flex-row items-start">
<div>
<UAIcon device={ua.device} className="w-10 h-10 text-blue-500" />
</div>
<div className="flex flex-col">
<span className="ml-2 text-lg font-semibold text-slate-800">
{ua.browser}
</span>
<span className="ml-2 text-sm text-gray-500">
{session.ipAddress}
</span>
</div>
</div>
<div className="col-span-12 order-3 sm:col-span-6 sm:order-2">
<div className="flex flex-col">
<span className="ml-2 text-sm font-medium text-gray-500">
{ua.os} {ua.osVersion}
</span>
<span className="ml-2 text-sm font-light text-gray-500">
Last access: <span className="font-medium">{lastActivity}</span>
</span>
</div>
</div>
<div className="col-span-4 order-2 sm:col-span-2 sm:order-3">
<div className="flex flex-row justify-end">
{session.current ? (
<span className="px-2 py-1 rounded-full bg-green-200 text-sm font-bold text-green-600">
Current
</span>
) : null}
</div>
</div>
</div>
</ItemBase> </ItemBase>
) )
} }

View File

@ -1,17 +1,144 @@
import { FunctionComponent, useCallback } from "react" import {
EventHandler,
FunctionComponent,
SyntheticEvent,
useCallback,
useEffect,
useMemo,
useState,
} from "react"
import { useDelete } from "../hooks/useCRUD" import { CheckCircleIcon, PencilSquareIcon } from "@heroicons/react/20/solid"
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline"
import { Form } from "react-router-dom"
import { useDelete, usePatch } from "../hooks/useCRUD"
import { Token } from "../types" import { Token } from "../types"
import { formatDateStr } from "../util/date"
import ItemBase from "./ItemBase" import ItemBase from "./ItemBase"
const TokenNameField: FunctionComponent<{ name: string; id: string }> = ({
name,
id,
}) => {
const [editing, setEditing] = useState(false)
const [newName, setNewName] = useState(name)
// Reset name when it changes
useEffect(() => {
setNewName(name)
setEditing(false)
}, [name])
// Handle submission
const [loading, patch] = usePatch()
const handleSubmit: EventHandler<SyntheticEvent> = useCallback(
(e) => {
e.preventDefault()
e.stopPropagation()
patch({ id: id, name: newName })
},
[newName, id, patch]
)
// This prevents blur from messing up submission when clicking the
// submit button directly
const stopBlur: EventHandler<SyntheticEvent> = useCallback((e) => {
e.preventDefault()
}, [])
// Enable/disable editing when the edit button is clicked
// and always reset the edit text to the current text
const handleEdit = useCallback(() => {
if (loading) return
setEditing((e) => !e)
setNewName(name)
}, [name, loading])
return (
<div
className="flex flex-row text-xl font-mono text-slate-600"
onBlur={handleEdit}>
{editing ? (
<Form onSubmit={handleSubmit} className="flex flex-row">
<input
style={{ width: `${newName.length + 1}ch` }}
className="border-b border-blue-500"
type="text"
value={newName}
disabled={loading}
onChange={(e) => setNewName(e.target.value)}
autoFocus
/>
<button
disabled={loading}
type="submit"
onMouseDown={stopBlur}
className="p-1 ml-1 text-green-700 rounded-full z-30 disabled:text-slate-600">
<CheckCircleIcon className="h-6 w-6" />
</button>
</Form>
) : (
<span className="flex flex-row">
<div className="flex flex-row items-center text-xl">
<span>{name}</span>
</div>
<button onClick={handleEdit} className="p-1 ml-4">
<PencilSquareIcon className="h-6 w-6" />
</button>
</span>
)}
</div>
)
}
const TokenValue: FunctionComponent<{ value: string }> = ({ value }) => {
const [show, setShow] = useState(false)
const breakStyle = useMemo(() => {
return show ? {} : { wordBreak: "break-word" as const }
}, [show])
return (
<div className="text-sm font-mono flex flex-row items-center break-all">
<span
className="bg-slate-600 text-slate-100 px-2 py-1 rounded-md"
style={breakStyle}>
{show ? value : "*".repeat(value.length)}
</span>
<button
onClick={() => setShow((s) => !s)}
className="ml-2 p-1 rounded-full text-slate-600">
{show ? (
<EyeSlashIcon className="h-6 w-6" />
) : (
<EyeIcon className="h-6 w-6" />
)}
</button>
</div>
)
}
const TokenItem: FunctionComponent<Token> = ({ ...token }) => { const TokenItem: FunctionComponent<Token> = ({ ...token }) => {
const [deleting, del] = useDelete() const [deleting, del] = useDelete()
const doDelete = useCallback(() => del({ id: token.id }), [del, token.id]) const doDelete = useCallback(() => del({ id: token.id }), [del, token.id])
const createdAt = formatDateStr(token.createdAt)
return ( return (
<ItemBase copyString={token.value} doDelete={doDelete} deleting={deleting}> <ItemBase copyString={token.value} doDelete={doDelete} deleting={deleting}>
{JSON.stringify(token)} <div className="grid grid-cols-12 gap-y-2 mb-2">
<div className="col-span-12">
<TokenNameField name={token.name} id={token.id} />
</div>
<div className="col-span-12 text-slate-500 font-light text-sm">
Created at: <span className="font-medium">{createdAt}</span>
</div>
<div className="col-span-12 w-fit">
<TokenValue value={token.value} />
</div>
</div>
</ItemBase> </ItemBase>
) )
} }

View File

@ -1,6 +1,6 @@
import { SetStateAction, useCallback, useEffect, useState } from "react" import { SetStateAction, useCallback, useEffect, useState } from "react"
import { useNavigation, useSubmit } from "react-router-dom" import { FormMethod, useNavigation, useSubmit } from "react-router-dom"
import { SubmitTarget } from "react-router-dom/dist/dom" import { SubmitTarget } from "react-router-dom/dist/dom"
import fetchAPI from "../util/fetchAPI" import fetchAPI from "../util/fetchAPI"
@ -73,58 +73,34 @@ export const useOnCreate = <T extends Record<string, unknown>>(
return [creating, create] as const return [creating, create] as const
} }
export const useCreate = <T extends SubmitTarget>( const makeUseVerb =
onCreate?: (payload?: T) => void (verb: Uppercase<FormMethod>) =>
) => { <T extends SubmitTarget>(onSubmit?: (payload?: T) => void) => {
const submit = useSubmit() const submit = useSubmit()
const navigation = useNavigation() const navigation = useNavigation()
const [creating, setCreating] = useState(false) const [submitting, setSubmitting] = useState(false)
const create = useCallback( const doSubmit = useCallback(
(payload?: T) => { (payload?: T) => {
setCreating(true) setSubmitting(true)
submit(payload ?? null, { submit(payload ?? null, {
method: "POST", method: verb,
replace: true, replace: true,
}) })
if (onCreate) onCreate(payload) if (onSubmit) onSubmit(payload)
}, },
[submit, onCreate] [submit, onSubmit]
) )
useEffect(() => { useEffect(() => {
if (navigation.state === "idle") { if (navigation.state === "idle") {
setCreating(false) setSubmitting(false)
} }
}, [navigation]) }, [navigation])
return [creating, create] as const return [submitting, doSubmit] as const
} }
export const useDelete = <T extends SubmitTarget>( export const useCreate = makeUseVerb("POST")
onDelete?: (payload: T) => void export const useDelete = makeUseVerb("DELETE")
) => { export const usePatch = makeUseVerb("PATCH")
const submit = useSubmit()
const navigation = useNavigation()
const [deleting, setDeleting] = useState(false)
const del = useCallback(
(payload: T) => {
setDeleting(true)
submit(payload ?? null, {
method: "DELETE",
replace: true,
})
if (onDelete) onDelete(payload)
},
[submit, onDelete]
)
useEffect(() => {
if (navigation.state === "idle") {
setDeleting(false)
}
}, [navigation])
return [deleting, del] as const
}

View File

@ -150,7 +150,7 @@ export const useVisitMetrics = (logs: ShortLog[]) => {
let last = logs[0].createdAt let last = logs[0].createdAt
logs.forEach((log) => { logs.forEach((log) => {
// If the log is older, update // If the log is older, update
if (log.createdAt.localeCompare(last) < 0) { if (log.createdAt.localeCompare(last) > 0) {
last = log.createdAt last = log.createdAt
} }
// Add IP to set // Add IP to set

View File

@ -187,7 +187,7 @@ export const Component: FunctionComponent = () => {
Choose a{" "} Choose a{" "}
<span className="text-green-500 font-bold">custom link</span> <span className="text-green-500 font-bold">custom link</span>
</span> </span>
<div className="bg-white text-sm xl:text-base sm:max-w-2xl ring-slate-300 ring-inset ring-1 shadow-sm rounded-md flex focus-within:ring-blue-500 outline-0 focus-within:ring-2 transition-all duration-200"> <div className="bg-white text-base sm:max-w-2xl ring-slate-300 ring-inset ring-1 shadow-sm rounded-md flex focus-within:ring-blue-500 outline-0 focus-within:ring-2 transition-all duration-200">
<span className=" text-slate-400 leading-5 pl-3 items-center select-none flex"> <span className=" text-slate-400 leading-5 pl-3 items-center select-none flex">
{`${location.host}/`} {`${location.host}/`}
</span> </span>
@ -212,9 +212,11 @@ export const Component: FunctionComponent = () => {
{shortening ? "Shortening..." : "Shorten it"} {shortening ? "Shortening..." : "Shorten it"}
</Button> </Button>
</form> </form>
<div className="mt-10"> {recentShorts.length ? (
<ItemList items={recentShorts} Item={ShortItem} idKey="id" /> <div className="mt-10">
</div> <ItemList items={recentShorts} Item={ShortItem} idKey="id" />
</div>
) : null}
</> </>
) )
} }

View File

@ -55,7 +55,7 @@ export function Component() {
maxLength={128} maxLength={128}
required required
placeholder="password" placeholder="password"
className="p-2 font-light border-b-2 bg-slate-50 border-slate-400 text-lg focus:outline-none focus:border-blue-500 transition-colors duration-200" className="p-2 border-b-2 bg-slate-50 border-slate-400 text-lg focus:outline-none focus:border-blue-500 transition-colors duration-200"
/> />
</label> </label>
<Button <Button

View File

@ -1,10 +1,9 @@
import { FunctionComponent, useCallback } from "react" import { useCallback } from "react"
import { LoaderFunction, redirect } from "react-router-dom" import { LoaderFunction, redirect } from "react-router-dom"
import Header from "../components/Header" import Header from "../components/Header"
import ItemList from "../components/ItemList" import ItemList from "../components/ItemList"
import NoItems from "../components/NoItems"
import SessionItem from "../components/SessionItem" import SessionItem from "../components/SessionItem"
import { protectedLoader } from "../hooks/useAuth" import { protectedLoader } from "../hooks/useAuth"
import { useSortedLoadedItems } from "../hooks/useLoaderItems" import { useSortedLoadedItems } from "../hooks/useLoaderItems"
@ -17,14 +16,10 @@ export function Component() {
useCallback((a, b) => b.lastActivity.localeCompare(a.lastActivity), []) useCallback((a, b) => b.lastActivity.localeCompare(a.lastActivity), [])
) )
const Sessions: FunctionComponent = () => {
return <ItemList items={items} Item={SessionItem} idKey="id" />
}
return ( return (
<> <>
<Header title="Sessions" /> <Header title="Sessions" />
{items.length > 0 ? <Sessions /> : <NoItems type="Session" />} <ItemList items={items} Item={SessionItem} idKey="id" />
</> </>
) )
} }

View File

@ -11,7 +11,7 @@ export function Component() {
return ( return (
<> <>
<BackButton className="absolute mt-10" /> <BackButton className="absolute mt-5" />
<Header title={short.name} /> <Header title={short.name} />
{short.url} {short.url}
</> </>

View File

@ -55,7 +55,7 @@ export function Component() {
maxLength={128} maxLength={128}
required required
placeholder="password" placeholder="password"
className="p-2 font-light border-b-2 bg-slate-50 border-slate-400 text-lg focus:outline-none focus:border-blue-500 transition-colors duration-200" className="p-2 border-b-2 bg-slate-50 border-slate-400 text-lg focus:outline-none focus:border-blue-500 transition-colors duration-200"
/> />
</label> </label>
<Button <Button

View File

@ -1,4 +1,4 @@
import { FunctionComponent, useCallback } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import { PlusIcon } from "@heroicons/react/24/outline" import { PlusIcon } from "@heroicons/react/24/outline"
import { LoaderFunction, redirect } from "react-router-dom" import { LoaderFunction, redirect } from "react-router-dom"
@ -18,19 +18,36 @@ export function Component() {
const [items] = useSortedLoadedItems<Token>( const [items] = useSortedLoadedItems<Token>(
useCallback((a, b) => b.createdAt.localeCompare(a.createdAt), []) useCallback((a, b) => b.createdAt.localeCompare(a.createdAt), [])
) )
const [nounlist, setNounList] = useState<string[]>(useMemo(() => [], []))
const [adjlist, setAdjlist] = useState<string[]>(useMemo(() => [], []))
useEffect(() => {
const fetchWords = async () => {
const nounsReq = await fetch("/nouns.txt")
const adjsReq = await fetch("/adjectives.txt")
if (nounsReq.ok && adjsReq.ok) {
const nouns = (await nounsReq.text()).split("\n")
const adjs = await (await adjsReq.text()).split("\n")
setNounList(nouns)
setAdjlist(adjs)
}
}
fetchWords()
}, [])
const generateName = useCallback(() => {
if (adjlist.length === 0 || nounlist.length === 0) return null
const adj = adjlist[Math.floor(Math.random() * adjlist.length)]
const noun = nounlist[Math.floor(Math.random() * nounlist.length)]
return `${adj} ${noun}`
}, [adjlist, nounlist])
// Handle creation // Handle creation
const [creating, create] = useCreate() const [creating, create] = useCreate()
const onClickCreate = useCallback(() => create(), [create]) const onClickCreate = useCallback(
() => create({ name: generateName() }),
const Tokens: FunctionComponent = () => { [create, generateName]
return <ItemList items={items} Item={TokenItem} idKey="id" /> )
}
const NoTokens = () => {
return (
<div className="text-center pt-5 text-xl font-light">No tokens yet</div>
)
}
return ( return (
<> <>
@ -42,7 +59,7 @@ export function Component() {
<PlusIcon className="inline-block w-5 h-5 mb-0.5 mr-1 leading-1" /> <PlusIcon className="inline-block w-5 h-5 mb-0.5 mr-1 leading-1" />
<span>New token</span> <span>New token</span>
</Button> </Button>
{items.length > 0 ? <Tokens /> : <NoTokens />} <ItemList items={items} Item={TokenItem} idKey="id" />
</> </>
) )
} }
@ -59,7 +76,14 @@ export const loader: LoaderFunction = async (args) => {
} }
export const action = crudAction({ export const action = crudAction({
POST: () => fetchAPI<Token>("/tokens", { method: "POST" }), POST: async (formData) => {
const name = formData.get("name") as string | null
return fetchAPI<Token>("/tokens", {
method: "POST",
body: JSON.stringify({ name }),
})
},
PATCH: async (formData) => { PATCH: async (formData) => {
const id = formData.get("id") as string | null const id = formData.get("id") as string | null
const name = formData.get("name") as string | null const name = formData.get("name") as string | null

View File

@ -0,0 +1,9 @@
export const formatDateStr = (dateStr: string): string => {
return new Date(Date.parse(dateStr)).toLocaleString("en", {
month: "long",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
})
}

16
go.mod
View File

@ -1,6 +1,6 @@
module git.maronato.dev/maronato/goshort module git.maronato.dev/maronato/goshort
go 1.20 go 1.21
require ( require (
github.com/alexedwards/scs/v2 v2.5.1 github.com/alexedwards/scs/v2 v2.5.1
@ -10,10 +10,10 @@ require (
github.com/peterbourgon/ff/v3 v3.4.0 github.com/peterbourgon/ff/v3 v3.4.0
github.com/uptrace/bun v1.1.14 github.com/uptrace/bun v1.1.14
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14 github.com/uptrace/bun/dialect/sqlitedialect v1.1.14
github.com/uptrace/bun/driver/sqliteshim v1.1.14
github.com/uptrace/bun/extra/bundebug v1.1.14 github.com/uptrace/bun/extra/bundebug v1.1.14
golang.org/x/crypto v0.12.0 golang.org/x/crypto v0.12.0
golang.org/x/sync v0.3.0 golang.org/x/sync v0.3.0
modernc.org/sqlite v1.25.0
) )
require ( require (
@ -25,22 +25,22 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/mod v0.10.0 // indirect golang.org/x/mod v0.3.0 // indirect
golang.org/x/sys v0.11.0 // indirect golang.org/x/sys v0.11.0 // indirect
golang.org/x/tools v0.9.1 // indirect golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect
lukechampine.com/uint128 v1.3.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.24.1 // indirect modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.6.0 // indirect modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.25.0 // indirect
modernc.org/strutil v1.1.3 // indirect modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect modernc.org/token v1.0.1 // indirect
) )

51
go.sum
View File

@ -4,6 +4,7 @@ github.com/alexedwards/scs/v2 v2.5.1 h1:EhAz3Kb3OSQzD8T+Ub23fKsiuvE0GzbF5Lgn0uTw
github.com/alexedwards/scs/v2 v2.5.1/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/alexedwards/scs/v2 v2.5.1/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
@ -15,7 +16,9 @@ github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vz
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -27,8 +30,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -39,43 +42,65 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.1.14 h1:S5vvNnjEynJ0CvnrBOD7MIRW7q/WbtvFXrdfy0lddAM= github.com/uptrace/bun v1.1.14 h1:S5vvNnjEynJ0CvnrBOD7MIRW7q/WbtvFXrdfy0lddAM=
github.com/uptrace/bun v1.1.14/go.mod h1:RHk6DrIisO62dv10pUOJCz5MphXThuOTpVNYEYv7NI8= github.com/uptrace/bun v1.1.14/go.mod h1:RHk6DrIisO62dv10pUOJCz5MphXThuOTpVNYEYv7NI8=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14 h1:SlwXLxr+N1kEo8Q0cheRlnIZLZlWniEB1OI+jkiLgWE= github.com/uptrace/bun/dialect/sqlitedialect v1.1.14 h1:SlwXLxr+N1kEo8Q0cheRlnIZLZlWniEB1OI+jkiLgWE=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.14/go.mod h1:9RTEj1l4bB9a4l1Mnc9y4COTwWlFYe1dh6fyxq1rR7A= github.com/uptrace/bun/dialect/sqlitedialect v1.1.14/go.mod h1:9RTEj1l4bB9a4l1Mnc9y4COTwWlFYe1dh6fyxq1rR7A=
github.com/uptrace/bun/driver/sqliteshim v1.1.14 h1:DFPUJ6KjDP2myjq15gtYYNngmAFMww1Y2UFZv4tbUw8=
github.com/uptrace/bun/driver/sqliteshim v1.1.14/go.mod h1:5BFN7V6Sm37Tn7UE4FWNm/F6V3iJPUzAJ7QyRwA5b1k=
github.com/uptrace/bun/extra/bundebug v1.1.14 h1:9OCGfP9ZDlh41u6OLerWdhBtJAVGXHr0xtxO4xWi6t0= github.com/uptrace/bun/extra/bundebug v1.1.14 h1:9OCGfP9ZDlh41u6OLerWdhBtJAVGXHr0xtxO4xWi6t0=
github.com/uptrace/bun/extra/bundebug v1.1.14/go.mod h1:lto3guzS2v6mnQp1+akyE+ecBLOltevDDe324NXEYdw= github.com/uptrace/bun/extra/bundebug v1.1.14/go.mod h1:lto3guzS2v6mnQp1+akyE+ecBLOltevDDe324NXEYdw=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
@ -89,6 +114,8 @@ modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=

View File

@ -18,12 +18,13 @@ const (
var DBTypes = [...]string{ var DBTypes = [...]string{
DBTypeMemory, DBTypeMemory,
DBTypeSQLite, DBTypeSQLite,
DBTypePostgres,
} }
const ( const (
// DefaultProd is the default mode.
DefaultProd = false
// DefaultDBType is the default type of database to use. // DefaultDBType is the default type of database to use.
DefaultDBType = DBTypeMemory DefaultDBType = DBTypeSQLite
// DefaultDBURL is the default connection string for the database. // DefaultDBURL is the default connection string for the database.
DefaultDBURL = "goshort.db" DefaultDBURL = "goshort.db"
// DefaultPort is the default port to listen on. // DefaultPort is the default port to listen on.

View File

@ -3,6 +3,8 @@ package apiserver
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"log/slog"
"net/http" "net/http"
"net/url" "net/url"
@ -10,25 +12,29 @@ import (
"git.maronato.dev/maronato/goshort/internal/server" "git.maronato.dev/maronato/goshort/internal/server"
servermiddleware "git.maronato.dev/maronato/goshort/internal/server/middleware" servermiddleware "git.maronato.dev/maronato/goshort/internal/server/middleware"
shortservice "git.maronato.dev/maronato/goshort/internal/service/short" shortservice "git.maronato.dev/maronato/goshort/internal/service/short"
shortlogservice "git.maronato.dev/maronato/goshort/internal/service/shortlog"
tokenservice "git.maronato.dev/maronato/goshort/internal/service/token" tokenservice "git.maronato.dev/maronato/goshort/internal/service/token"
userservice "git.maronato.dev/maronato/goshort/internal/service/user" userservice "git.maronato.dev/maronato/goshort/internal/service/user"
"git.maronato.dev/maronato/goshort/internal/storage/models" "git.maronato.dev/maronato/goshort/internal/storage/models"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
) )
type APIHandler struct { type APIHandler struct {
shorts *shortservice.ShortService shorts *shortservice.ShortService
users *userservice.UserService users *userservice.UserService
tokens *tokenservice.TokenService tokens *tokenservice.TokenService
shortLogs *shortlogservice.ShortLogService
} }
func NewAPIHandler(shorts *shortservice.ShortService, users *userservice.UserService, tokens *tokenservice.TokenService) *APIHandler { func NewAPIHandler(shorts *shortservice.ShortService, users *userservice.UserService, tokens *tokenservice.TokenService, shortLogs *shortlogservice.ShortLogService) *APIHandler {
return &APIHandler{ return &APIHandler{
shorts: shorts, shorts: shorts,
users: users, users: users,
tokens: tokens, tokens: tokens,
shortLogs: shortLogs,
} }
} }
@ -74,6 +80,7 @@ func (h *APIHandler) DeleteMe(w http.ResponseWriter, r *http.Request) {
func (h *APIHandler) Login(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
l := logging.FromCtx(ctx)
type loginForm struct { type loginForm struct {
Username string `json:"username"` Username string `json:"username"`
@ -93,6 +100,8 @@ func (h *APIHandler) Login(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
if errors.Is(err, errs.ErrUserDoesNotExist) || errors.Is(err, errs.ErrFailedAuthentication) { if errors.Is(err, errs.ErrUserDoesNotExist) || errors.Is(err, errs.ErrFailedAuthentication) {
// If the username or password are wrong, return invalid username/password // If the username or password are wrong, return invalid username/password
l.Debug("failed to authenticate user", "err", err)
server.RenderUnauthorized(w, r) server.RenderUnauthorized(w, r)
} else if errors.Is(err, errs.ErrInvalidUser) { } else if errors.Is(err, errs.ErrInvalidUser) {
// If the request was invalid, return bad request // If the request was invalid, return bad request
@ -126,6 +135,7 @@ func (h *APIHandler) Logout(w http.ResponseWriter, r *http.Request) {
func (h *APIHandler) Signup(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) Signup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
l := logging.FromCtx(ctx)
// Get the user from the json body // Get the user from the json body
type signupForm struct { type signupForm struct {
@ -162,6 +172,8 @@ func (h *APIHandler) Signup(w http.ResponseWriter, r *http.Request) {
if errors.Is(err, errs.ErrUserExists) || errors.Is(err, errs.ErrInvalidUser) { if errors.Is(err, errs.ErrUserExists) || errors.Is(err, errs.ErrInvalidUser) {
server.RenderBadRequest(w, r, err) server.RenderBadRequest(w, r, err)
} else if errors.Is(err, errs.ErrRegistrationDisabled) { } else if errors.Is(err, errs.ErrRegistrationDisabled) {
l.Debug("failed to create user", "err", err)
server.RenderForbidden(w, r) server.RenderForbidden(w, r)
} else { } else {
server.RenderServerError(w, r, err) server.RenderServerError(w, r, err)
@ -293,7 +305,7 @@ func (h *APIHandler) ListShortLogs(w http.ResponseWriter, r *http.Request) {
} }
// Get logs // Get logs
logs, err := h.shorts.ListLogs(ctx, short) logs, err := h.shortLogs.ListLogs(ctx, short)
if err != nil { if err != nil {
server.RenderServerError(w, r, err) server.RenderServerError(w, r, err)
@ -326,6 +338,7 @@ func (h *APIHandler) ListSessions(w http.ResponseWriter, r *http.Request) {
func (h *APIHandler) DeleteSession(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) DeleteSession(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
l := logging.FromCtx(ctx)
// Get user from context // Get user from context
user, ok := h.findUserOrRespond(w, r) user, ok := h.findUserOrRespond(w, r)
@ -340,6 +353,8 @@ func (h *APIHandler) DeleteSession(w http.ResponseWriter, r *http.Request) {
err := servermiddleware.DeleteUserSession(ctx, user, sessionToken) err := servermiddleware.DeleteUserSession(ctx, user, sessionToken)
if err != nil { if err != nil {
if errors.Is(err, errs.ErrSessionDoesNotExist) { if errors.Is(err, errs.ErrSessionDoesNotExist) {
l.Debug("could not delete session", "err", err)
server.RenderNotFound(w, r) server.RenderNotFound(w, r)
} else { } else {
server.RenderServerError(w, r, err) server.RenderServerError(w, r, err)
@ -377,13 +392,25 @@ func (h *APIHandler) ListTokens(w http.ResponseWriter, r *http.Request) {
func (h *APIHandler) CreateToken(w http.ResponseWriter, r *http.Request) { func (h *APIHandler) CreateToken(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
type tokenNameForm struct {
Name string `json:"name"`
}
form := &tokenNameForm{}
// if error is EOF, it means the body is empty, so we can ignore it
if err := render.DecodeJSON(r.Body, form); err != nil && err != io.EOF {
server.RenderBadRequest(w, r, err)
return
}
// Get user from context // Get user from context
user, ok := h.findUserOrRespond(w, r) user, ok := h.findUserOrRespond(w, r)
if !ok { if !ok {
return return
} }
token, err := h.tokens.CreateToken(ctx, user) token, err := h.tokens.CreateToken(ctx, user, form.Name)
if err != nil { if err != nil {
server.RenderServerError(w, r, err) server.RenderServerError(w, r, err)
@ -472,6 +499,7 @@ func (h *APIHandler) findUserOrRespond(w http.ResponseWriter, r *http.Request) (
// the short and true. If it isn't, it returns nil and false. // the short and true. If it isn't, it returns nil and false.
func (h *APIHandler) findShortOrRespond(w http.ResponseWriter, r *http.Request) (short *models.Short, ok bool) { func (h *APIHandler) findShortOrRespond(w http.ResponseWriter, r *http.Request) (short *models.Short, ok bool) {
ctx := r.Context() ctx := r.Context()
l := logging.FromCtx(ctx)
// Get short id from request // Get short id from request
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
@ -481,6 +509,8 @@ func (h *APIHandler) findShortOrRespond(w http.ResponseWriter, r *http.Request)
if err != nil { if err != nil {
// If the short doesn't exist or is invalid, return not found // If the short doesn't exist or is invalid, return not found
if errors.Is(err, errs.ErrShortDoesNotExist) || errors.Is(err, errs.ErrInvalidShort) { if errors.Is(err, errs.ErrShortDoesNotExist) || errors.Is(err, errs.ErrInvalidShort) {
l.Debug("could not find short", "err", err)
server.RenderNotFound(w, r) server.RenderNotFound(w, r)
} else { } else {
server.RenderServerError(w, r, err) server.RenderServerError(w, r, err)
@ -498,6 +528,8 @@ func (h *APIHandler) findShortOrRespond(w http.ResponseWriter, r *http.Request)
// If the session user does not match the short's user, // If the session user does not match the short's user,
// return forbidden. // return forbidden.
if user.ID != *short.UserID { if user.ID != *short.UserID {
l.Debug("short's user does not match request user", slog.String("short_user", *short.UserID))
server.RenderForbidden(w, r) server.RenderForbidden(w, r)
return nil, false return nil, false
@ -511,6 +543,7 @@ func (h *APIHandler) findShortOrRespond(w http.ResponseWriter, r *http.Request)
// the token and true. If it isn't, it returns nil and false. // the token and true. If it isn't, it returns nil and false.
func (h *APIHandler) findTokenOrRespond(w http.ResponseWriter, r *http.Request) (token *models.Token, ok bool) { func (h *APIHandler) findTokenOrRespond(w http.ResponseWriter, r *http.Request) (token *models.Token, ok bool) {
ctx := r.Context() ctx := r.Context()
l := logging.FromCtx(ctx)
// Get token ID from request // Get token ID from request
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
@ -520,6 +553,8 @@ func (h *APIHandler) findTokenOrRespond(w http.ResponseWriter, r *http.Request)
if err != nil { if err != nil {
// If the token doesn't exist or is invalid, return not found // If the token doesn't exist or is invalid, return not found
if errors.Is(err, errs.ErrTokenDoesNotExist) || errors.Is(err, errs.ErrInvalidToken) { if errors.Is(err, errs.ErrTokenDoesNotExist) || errors.Is(err, errs.ErrInvalidToken) {
l.Debug("could not find token", "err", err)
server.RenderNotFound(w, r) server.RenderNotFound(w, r)
} else { } else {
server.RenderServerError(w, r, err) server.RenderServerError(w, r, err)
@ -537,6 +572,8 @@ func (h *APIHandler) findTokenOrRespond(w http.ResponseWriter, r *http.Request)
// If the session user does not match the token's user, // If the session user does not match the token's user,
// return NotFound. // return NotFound.
if user.ID != *token.UserID { if user.ID != *token.UserID {
l.Debug("token's user does not match request user", slog.String("token_user", *token.UserID))
server.RenderNotFound(w, r) server.RenderNotFound(w, r)
return nil, false return nil, false

View File

@ -3,6 +3,7 @@ package devuiserver
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/url" "net/url"
"os" "os"
@ -10,6 +11,7 @@ import (
"syscall" "syscall"
"git.maronato.dev/maronato/goshort/internal/config" "git.maronato.dev/maronato/goshort/internal/config"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -41,8 +43,11 @@ func NewServer(cfg *config.Config) *Server {
} }
func (s *Server) ListenAndServe(ctx context.Context) error { func (s *Server) ListenAndServe(ctx context.Context) error {
l := logging.FromCtx(ctx)
eg, egCtx := errgroup.WithContext(ctx) eg, egCtx := errgroup.WithContext(ctx)
eg.Go(func() error { eg.Go(func() error {
l.Info("Starting UI dev server", slog.String("addr", net.JoinHostPort(s.host, s.uiPort)))
return s.Start(egCtx) return s.Start(egCtx)
}) })
@ -50,6 +55,8 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
// Wait for the context to be done // Wait for the context to be done
<-egCtx.Done() <-egCtx.Done()
// Shutdown the server // Shutdown the server
l.Info("Shutting down UI dev server")
return s.Shutdown() return s.Shutdown()
}) })
@ -61,6 +68,8 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
} }
func (s *Server) Start(ctx context.Context) error { func (s *Server) Start(ctx context.Context) error {
l := logging.FromCtx(ctx)
// Create a new context with a cancel function so we can stop the server // Create a new context with a cancel function so we can stop the server
uiCtx, cancel := context.WithCancel(ctx) uiCtx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
@ -109,6 +118,8 @@ func (s *Server) Start(ctx context.Context) error {
return fmt.Errorf("error killing process group: %w", err) return fmt.Errorf("error killing process group: %w", err)
} }
l.Info("UI dev server shutdown complete")
return nil return nil
}) })

View File

@ -43,10 +43,6 @@ func ErrBadRequest(err error) render.Renderer {
return ErrGeneric(err, http.StatusBadRequest) return ErrGeneric(err, http.StatusBadRequest)
} }
func ErrServerError(err error) render.Renderer {
return ErrGeneric(err, http.StatusInternalServerError)
}
func ErrNotFound() render.Renderer { func ErrNotFound() render.Renderer {
return ErrBasic(http.StatusNotFound) return ErrBasic(http.StatusNotFound)
} }
@ -84,7 +80,8 @@ func RenderBadRequest(w http.ResponseWriter, r *http.Request, err error) {
} }
func RenderServerError(w http.ResponseWriter, r *http.Request, err error) { func RenderServerError(w http.ResponseWriter, r *http.Request, err error) {
RenderRender(w, r, ErrServerError(err)) // Panic so the stack trace is printed
panic(err)
} }
func RenderNotFound(w http.ResponseWriter, r *http.Request) { func RenderNotFound(w http.ResponseWriter, r *http.Request) {

View File

@ -3,32 +3,74 @@ package healthcheckserver
import ( import (
"net/http" "net/http"
"git.maronato.dev/maronato/goshort/internal/storage"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"github.com/go-chi/render" "github.com/go-chi/render"
) )
type HealthcheckHandler struct{} type HealthCheckStatus string
func NewHealthcheckHandler() *HealthcheckHandler { const (
HealthCheckStatusOk HealthCheckStatus = "ok"
HealthCheckStatusDegraded HealthCheckStatus = "degraded"
HealthCheckStatusDown HealthCheckStatus = "down"
)
return &HealthcheckHandler{} type HealthcheckHandler struct {
storage storage.Storage
}
func NewHealthcheckHandler(storage storage.Storage) *HealthcheckHandler {
return &HealthcheckHandler{
storage: storage,
}
} }
func (h *HealthcheckHandler) CheckHealth(w http.ResponseWriter, r *http.Request) { func (h *HealthcheckHandler) CheckHealth(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
l := logging.FromCtx(ctx)
// Defaults
databaseOk := true
serverOk := true
// Check if storage is ok
if err := h.storage.Ping(ctx); err != nil {
l.Error("error pinging database", err)
databaseOk = false
}
// All checks must pass
overallStatus := HealthCheckStatusOk
statusCode := http.StatusOK
if !databaseOk || !serverOk {
overallStatus = HealthCheckStatusDegraded
statusCode = http.StatusInternalServerError
} else if !databaseOk && !serverOk {
// Down is never going to be the case, since to respond
// the server has to be running. This is just for the sake
// of completeness.
overallStatus = HealthCheckStatusDown
statusCode = http.StatusServiceUnavailable
}
// Create the response // Create the response
response := &healthCheckResponse{ response := &healthCheckResponse{
Status: "ok", Status: overallStatus,
ServerOK: true, ServerOK: serverOk,
DatabaseOK: true, DatabaseOK: databaseOk,
} }
// Render the response // Render the response
render.Status(r, http.StatusOK) render.Status(r, statusCode)
render.JSON(w, r, response) render.JSON(w, r, response)
} }
type healthCheckResponse struct { type healthCheckResponse struct {
// Status is the status of the health check // Status is the status of the health check
Status string `json:"status"` Status HealthCheckStatus `json:"status"`
// ServerOK is the status of the server // ServerOK is the status of the server
ServerOK bool `json:"server_ok"` ServerOK bool `json:"server_ok"`
// DatabaseOK is the status of the database // DatabaseOK is the status of the database

View File

@ -0,0 +1,74 @@
package servermiddleware
import (
"log/slog"
"net/http"
"time"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"github.com/go-chi/chi/v5/middleware"
)
type RequestLogFormatter struct{}
func NewLogFormatter() *RequestLogFormatter {
return &RequestLogFormatter{}
}
// RequestLogEntry is a struct that implements the LogEntry interface
type RequestLogEntry struct {
l *slog.Logger
}
func (rl *RequestLogFormatter) NewLogEntry(r *http.Request) middleware.LogEntry {
ctx := r.Context()
l := logging.FromCtx(ctx)
reqID := middleware.GetReqID(ctx)
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
requestGroup := slog.Group("request",
slog.String("id", reqID),
slog.String("scheme", scheme),
slog.String("host", r.Host),
slog.String("method", r.Method),
slog.String("remote_addr", r.RemoteAddr),
slog.String("path", r.URL.Path),
slog.String("user_agent", r.UserAgent()),
slog.String("referer", r.Referer()),
slog.String("content_type", r.Header.Get("Content-Type")),
slog.Int64("content_length", r.ContentLength),
)
l = l.With(requestGroup)
return &RequestLogEntry{
l: l,
}
}
func (le *RequestLogEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
responseGroup := slog.Group("response",
slog.Int("status", status),
slog.Duration("duration", elapsed),
slog.Int("content_length", bytes),
slog.String("content_type", header.Get("Content-Type")),
)
l := le.l.With(responseGroup)
switch {
case status >= http.StatusInternalServerError:
l.Error("Server error")
case status >= http.StatusBadRequest:
l.Info("Client error")
default:
l.Info("Request completed")
}
}
func (le *RequestLogEntry) Panic(v interface{}, stack []byte) {
middleware.PrintPrettyStack(v)
}

View File

@ -20,7 +20,7 @@ func SessionManager(cfg *config.Config) func(http.Handler) http.Handler {
Path: "/", Path: "/",
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
Persist: true, Persist: true,
// Secure: cfg.Prod, Secure: cfg.Prod,
} }
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {

View File

@ -3,11 +3,13 @@ package server
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"net" "net"
"net/http" "net/http"
"git.maronato.dev/maronato/goshort/internal/config" "git.maronato.dev/maronato/goshort/internal/config"
servermiddleware "git.maronato.dev/maronato/goshort/internal/server/middleware" servermiddleware "git.maronato.dev/maronato/goshort/internal/server/middleware"
"git.maronato.dev/maronato/goshort/internal/util/logging"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -26,10 +28,11 @@ func NewServer(cfg *config.Config) *Server {
mux := chi.NewRouter() mux := chi.NewRouter()
// Register default middlewares // Register default middlewares
mux.Use(middleware.Recoverer) requestLogger := servermiddleware.NewLogFormatter()
mux.Use(middleware.RequestID) mux.Use(middleware.RequestID)
mux.Use(middleware.RealIP) mux.Use(middleware.RealIP)
mux.Use(middleware.Logger) mux.Use(middleware.RequestLogger(requestLogger))
mux.Use(middleware.Recoverer)
mux.Use(servermiddleware.SessionManager(cfg)) mux.Use(servermiddleware.SessionManager(cfg))
mux.Use(middleware.Timeout(config.RequestTimeout)) mux.Use(middleware.Timeout(config.RequestTimeout))
mux.Use(middleware.Compress(5, "application/json")) mux.Use(middleware.Compress(5, "application/json"))
@ -51,11 +54,19 @@ func NewServer(cfg *config.Config) *Server {
} }
func (s *Server) ListenAndServe(ctx context.Context) error { func (s *Server) ListenAndServe(ctx context.Context) error {
l := logging.FromCtx(ctx)
// Create the errorgroup that will manage the server execution // Create the errorgroup that will manage the server execution
eg, egCtx := errgroup.WithContext(ctx) eg, egCtx := errgroup.WithContext(ctx)
// Start the server // Start the server
eg.Go(func() error { eg.Go(func() error {
l.Info("Starting server", slog.String("addr", s.srv.Addr))
// Use the global context for the server
s.srv.BaseContext = func(_ net.Listener) context.Context {
return egCtx
}
return s.srv.ListenAndServe() return s.srv.ListenAndServe()
}) })
@ -64,11 +75,17 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
// Wait for the context to be done // Wait for the context to be done
<-egCtx.Done() <-egCtx.Done()
l.Info("Shutting down server")
return s.srv.Shutdown( return s.srv.Shutdown(
context.Background(), context.Background(),
) )
}) })
s.srv.RegisterOnShutdown(func() {
l.Info("Server shutdown complete")
})
// Ignore the error if the context was canceled // Ignore the error if the context was canceled
if err := eg.Wait(); err != nil && ctx.Err() == nil { if err := eg.Wait(); err != nil && ctx.Err() == nil {
return fmt.Errorf("server exited with error: %w", err) return fmt.Errorf("server exited with error: %w", err)

View File

@ -2,23 +2,25 @@ package shortserver
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"git.maronato.dev/maronato/goshort/internal/errs" "git.maronato.dev/maronato/goshort/internal/errs"
"git.maronato.dev/maronato/goshort/internal/server" "git.maronato.dev/maronato/goshort/internal/server"
shortservice "git.maronato.dev/maronato/goshort/internal/service/short" shortservice "git.maronato.dev/maronato/goshort/internal/service/short"
shortlogservice "git.maronato.dev/maronato/goshort/internal/service/shortlog"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
type ShortHandler struct { type ShortHandler struct {
service *shortservice.ShortService shorts *shortservice.ShortService
shortLogs *shortlogservice.ShortLogService
} }
func NewShortHandler(service *shortservice.ShortService) *ShortHandler { func NewShortHandler(shorts *shortservice.ShortService, shortLogs *shortlogservice.ShortLogService) *ShortHandler {
return &ShortHandler{ return &ShortHandler{
service: service, shorts: shorts,
shortLogs: shortLogs,
} }
} }
@ -29,26 +31,26 @@ func (h *ShortHandler) FindShort(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "short") name := chi.URLParam(r, "short")
// Get the URL from the service // Get the URL from the service
short, err := h.service.FindShort(ctx, name) short, err := h.shorts.FindShort(ctx, name)
switch { switch {
case err == nil: case err == nil:
// If there's no error, log the access and redirect to the URL // If there's no error, log the access and redirect to the URL
err = h.service.LogShortAccess(ctx, short, r) h.shortLogs.LogShortAccess(ctx, short, r)
if err != nil {
// If there was an error logging the access, print a message and
// continue.
fmt.Printf("failed to log short access: %v\n", err)
}
http.Redirect(w, r, short.URL, http.StatusSeeOther) http.Redirect(w, r, short.URL, http.StatusSeeOther)
case errors.Is(err, errs.ErrInvalidShort): case errors.Is(err, errs.ErrInvalidShort):
// If the short name is invalid, do nothing and let the static handler // If the short name is invalid, do nothing and let the static handler
// take care of it. // take care of it.
case errors.Is(err, errs.ErrShortDoesNotExist): case errors.Is(err, errs.ErrShortDoesNotExist):
// If the short doesn't exist, do nothing and let the static handler // If the short doesn't exist, do nothing and let the static handler
// take care of it. // take care of it.
default: default:
// Oops, this shouldn't happen. // Oops, this shouldn't happen.
server.RenderRender(w, r, server.ErrServerError(err)) server.RenderServerError(w, r, err)
} }
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"regexp" "regexp"
@ -33,33 +32,6 @@ func NewShortService(db storage.Storage) *ShortService {
return &ShortService{db: db} return &ShortService{db: db}
} }
func (s *ShortService) LogShortAccess(ctx context.Context, short *models.Short, r *http.Request) error {
// Log the access
shortLog := &models.ShortLog{
ShortID: short.ID,
IPAddress: r.RemoteAddr,
UserAgent: r.UserAgent(),
Referer: r.Referer(),
}
err := s.db.CreateShortLog(ctx, shortLog)
if err != nil {
return fmt.Errorf("failed to log short access: %w", err)
}
return nil
}
func (s *ShortService) ListLogs(ctx context.Context, short *models.Short) ([]*models.ShortLog, error) {
// Get the logs from storage
logs, err := s.db.ListShortLogs(ctx, short)
if err != nil {
return nil, fmt.Errorf("failed to get short logs from storage: %w", err)
}
return logs, nil
}
func (s *ShortService) FindShort(ctx context.Context, name string) (*models.Short, error) { func (s *ShortService) FindShort(ctx context.Context, name string) (*models.Short, error) {
// Check if the short is valid // Check if the short is valid
err := ShortNameIsValid(name) err := ShortNameIsValid(name)

View File

@ -0,0 +1,88 @@
package shortlogservice
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"git.maronato.dev/maronato/goshort/internal/storage"
"git.maronato.dev/maronato/goshort/internal/storage/models"
"git.maronato.dev/maronato/goshort/internal/util/logging"
)
type ShortLogService struct {
db storage.Storage
logCh chan *models.ShortLog
}
func NewShortLogService(db storage.Storage) *ShortLogService {
logCh := make(chan *models.ShortLog, 100)
return &ShortLogService{
db: db,
logCh: logCh,
}
}
func (s *ShortLogService) StartWorker(ctx context.Context) context.CancelFunc {
workerCtx, cancel := context.WithCancel(ctx)
// Start the goroutine
go s.shortLogWorker(workerCtx)
return cancel
}
func (s *ShortLogService) shortLogWorker(ctx context.Context) {
l := logging.FromCtx(ctx)
l.Debug("starting short log worker")
for {
select {
case <-ctx.Done():
l.Debug("stopping short log worker")
return
case shortLog := <-s.logCh:
l.Debug("writing short log to storage", slog.String("short_id", shortLog.ShortID))
err := s.db.CreateShortLog(ctx, shortLog)
if err != nil {
l.Warn("failed to log short access", "error", err)
}
}
}
}
func (s *ShortLogService) LogShortAccess(ctx context.Context, short *models.Short, r *http.Request) {
l := logging.FromCtx(ctx)
// Log the access
shortLog := &models.ShortLog{
ShortID: short.ID,
IPAddress: r.RemoteAddr,
UserAgent: r.UserAgent(),
Referer: r.Referer(),
CreatedAt: time.Now(),
}
l.Debug("adding short log to queue", slog.String("short_id", short.ID))
// Send the log to the channel
s.logCh <- shortLog
return
}
func (s *ShortLogService) ListLogs(ctx context.Context, short *models.Short) ([]*models.ShortLog, error) {
// Get the logs from storage
logs, err := s.db.ListShortLogs(ctx, short)
if err != nil {
return nil, fmt.Errorf("failed to get short logs from storage: %w", err)
}
return logs, nil
}

View File

@ -15,7 +15,7 @@ const (
// DefaultTokenLength is the default length of a token. // DefaultTokenLength is the default length of a token.
TokenLength = 32 TokenLength = 32
// TokenPrefix is the prefix of a token. // TokenPrefix is the prefix of a token.
TokenPrefix = "goshort-token:" TokenPrefix = "gst_"
// DefaultTokenIDLength is the default length of a token ID. // DefaultTokenIDLength is the default length of a token ID.
TokenIDLength = 16 TokenIDLength = 16
// MinTokenNameLength is the minimum length of a token name. // MinTokenNameLength is the minimum length of a token name.
@ -83,10 +83,15 @@ func (s *TokenService) ListTokens(ctx context.Context, user *models.User) ([]*mo
} }
// CreateToken creates a new token for a user. // CreateToken creates a new token for a user.
func (s *TokenService) CreateToken(ctx context.Context, user *models.User) (*models.Token, error) { func (s *TokenService) CreateToken(ctx context.Context, user *models.User, name string) (*models.Token, error) {
if name == "" {
// Make sure the name is valid
name = fmt.Sprintf("%s's token", user.Username)
}
// Generate a new token // Generate a new token
token := &models.Token{ token := &models.Token{
Name: fmt.Sprintf("%s's token", user.Username), Name: name,
Value: TokenPrefix + tokenutil.GenerateSecureToken(TokenLength/2), Value: TokenPrefix + tokenutil.GenerateSecureToken(TokenLength/2),
UserID: &user.ID, UserID: &user.ID,
} }

View File

@ -46,7 +46,7 @@ type ShortModel struct {
// CreatedAt is when the short was created (initialized by the storage) // CreatedAt is when the short was created (initialized by the storage)
CreatedAt time.Time `bun:",notnull,default:current_timestamp" json:"createdAt"` CreatedAt time.Time `bun:",notnull,default:current_timestamp" json:"createdAt"`
// DeletedAt is when the short was deleted // DeletedAt is when the short was deleted
DeletedAt time.Time `bun:",null" json:"-"` DeletedAt time.Time `bun:"," json:"-"`
// UserID is the ID of the user that created the short // UserID is the ID of the user that created the short
// This can be null if the short was deleted // This can be null if the short was deleted

View File

@ -3,6 +3,7 @@ package bunstorage
import ( import (
"context" "context"
"database/sql" "database/sql"
"strings"
"time" "time"
"git.maronato.dev/maronato/goshort/internal/config" "git.maronato.dev/maronato/goshort/internal/config"
@ -143,6 +144,10 @@ func (s *BunStorage) Stop(ctx context.Context) error {
return s.db.Close() return s.db.Close()
} }
func (s *BunStorage) Ping(ctx context.Context) error {
return s.db.PingContext(ctx)
}
func (s *BunStorage) FindShort(ctx context.Context, name string) (*models.Short, error) { func (s *BunStorage) FindShort(ctx context.Context, name string) (*models.Short, error) {
shortModel := new(ShortModel) shortModel := new(ShortModel)
@ -199,6 +204,10 @@ func (s *BunStorage) CreateShort(ctx context.Context, short *models.Short) (*mod
Model(shortModel). Model(shortModel).
Exec(ctx) Exec(ctx)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
err = errs.ErrUserExists
}
return nil, errs.Errorf("failed to create short", err) return nil, errs.Errorf("failed to create short", err)
} }
@ -240,9 +249,9 @@ func (s *BunStorage) ListShorts(ctx context.Context, user *models.User) ([]*mode
return nil, errs.Errorf("failed to list shorts", err) return nil, errs.Errorf("failed to list shorts", err)
} }
shorts := []*models.Short{} shorts := make([]*models.Short, len(shortModels))
for _, shortModel := range shortModels { for i, shortModel := range shortModels {
shorts = append(shorts, shortModel.toShort()) shorts[i] = shortModel.toShort()
} }
return shorts, nil return shorts, nil
} }
@ -269,6 +278,10 @@ func (s *BunStorage) CreateShortLog(ctx context.Context, shortLog *models.ShortL
Model(shortLogModel). Model(shortLogModel).
Exec(ctx) Exec(ctx)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
err = errs.ErrUserExists
}
return errs.Errorf("failed to create short log", err) return errs.Errorf("failed to create short log", err)
} }
@ -285,9 +298,9 @@ func (s *BunStorage) ListShortLogs(ctx context.Context, short *models.Short) ([]
return nil, errs.Errorf("failed to list short logs", err) return nil, errs.Errorf("failed to list short logs", err)
} }
shortLogs := []*models.ShortLog{} shortLogs := make([]*models.ShortLog, len(shortLogModels))
for _, shortLogModel := range shortLogModels { for i, shortLogModel := range shortLogModels {
shortLogs = append(shortLogs, shortLogModel.toShortLog()) shortLogs[i] = shortLogModel.toShortLog()
} }
return shortLogs, nil return shortLogs, nil
} }
@ -330,6 +343,10 @@ func (s *BunStorage) CreateUser(ctx context.Context, user *models.User) (*models
Model(userModel). Model(userModel).
Exec(ctx) Exec(ctx)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
err = errs.ErrUserExists
}
return nil, errs.Errorf("failed to create user", err) return nil, errs.Errorf("failed to create user", err)
} }
@ -406,9 +423,9 @@ func (s *BunStorage) ListTokens(ctx context.Context, user *models.User) ([]*mode
return nil, errs.Errorf("failed to list tokens", err) return nil, errs.Errorf("failed to list tokens", err)
} }
tokens := []*models.Token{} tokens := make([]*models.Token, len(tokenModels))
for _, tokenModel := range tokenModels { for i, tokenModel := range tokenModels {
tokens = append(tokens, tokenModel.toToken()) tokens[i] = tokenModel.toToken()
} }
return tokens, nil return tokens, nil
@ -435,6 +452,10 @@ func (s *BunStorage) CreateToken(ctx context.Context, token *models.Token) (*mod
Model(tokenModel). Model(tokenModel).
Exec(ctx) Exec(ctx)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
err = errs.ErrUserExists
}
return nil, errs.Errorf("failed to create token", err) return nil, errs.Errorf("failed to create token", err)
} }
@ -460,6 +481,7 @@ func (s *BunStorage) ChangeTokenName(ctx context.Context, token *models.Token, n
Model((*TokenModel)(nil)). Model((*TokenModel)(nil)).
Set("name = ?", name). Set("name = ?", name).
Where("id = ?", token.ID). Where("id = ?", token.ID).
Returning("*").
Exec(ctx, newToken) Exec(ctx, newToken)
if err != nil { if err != nil {
return nil, errs.Errorf("failed to change token name", err) return nil, errs.Errorf("failed to change token name", err)

View File

@ -11,6 +11,7 @@ import (
"git.maronato.dev/maronato/goshort/internal/errs" "git.maronato.dev/maronato/goshort/internal/errs"
"git.maronato.dev/maronato/goshort/internal/storage" "git.maronato.dev/maronato/goshort/internal/storage"
"git.maronato.dev/maronato/goshort/internal/storage/models" "git.maronato.dev/maronato/goshort/internal/storage/models"
"git.maronato.dev/maronato/goshort/internal/util/logging"
) )
// MemoryStorage is a storage that stores everything in memory. // MemoryStorage is a storage that stores everything in memory.
@ -48,26 +49,28 @@ func NewMemoryStorage(cfg *config.Config) *MemoryStorage {
} }
// logPerformance is a helper function to log the performance of a function. // logPerformance is a helper function to log the performance of a function.
func (s *MemoryStorage) logPerformance() func() { func (s *MemoryStorage) logPerformance(ctx context.Context) func() {
start := time.Now() start := time.Now()
return func() { return func() {
elapsed := time.Since(start) took := time.Since(start)
if s.debug { if s.debug {
l := logging.FromCtx(ctx)
pc, _, _, ok := runtime.Caller(1) pc, _, _, ok := runtime.Caller(1)
if !ok { if !ok {
return return
} }
method := runtime.FuncForPC(pc).Name() method := runtime.FuncForPC(pc).Name()
fmt.Printf("%s took %s\n", method, elapsed) l.Debug("MemoryStorage performance", "method", method, "took", took)
} }
} }
} }
// Start starts the storage. // Start starts the storage.
func (s *MemoryStorage) Start(ctx context.Context) error { func (s *MemoryStorage) Start(ctx context.Context) error {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
return nil return nil
@ -75,7 +78,14 @@ func (s *MemoryStorage) Start(ctx context.Context) error {
// Stop stops the storage. // Stop stops the storage.
func (s *MemoryStorage) Stop(ctx context.Context) error { func (s *MemoryStorage) Stop(ctx context.Context) error {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf()
return nil
}
func (s *MemoryStorage) Ping(ctx context.Context) error {
logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
return nil return nil
@ -83,7 +93,7 @@ func (s *MemoryStorage) Stop(ctx context.Context) error {
// FindShort finds a short in the storage. // FindShort finds a short in the storage.
func (s *MemoryStorage) FindShort(ctx context.Context, name string) (*models.Short, error) { func (s *MemoryStorage) FindShort(ctx context.Context, name string) (*models.Short, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.shortMu.RLock() s.shortMu.RLock()
@ -99,7 +109,7 @@ func (s *MemoryStorage) FindShort(ctx context.Context, name string) (*models.Sho
// FindShortByID finds a short in the storage. // FindShortByID finds a short in the storage.
func (s *MemoryStorage) FindShortByID(ctx context.Context, id string) (*models.Short, error) { func (s *MemoryStorage) FindShortByID(ctx context.Context, id string) (*models.Short, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.shortIDMu.RLock() s.shortIDMu.RLock()
@ -114,7 +124,7 @@ func (s *MemoryStorage) FindShortByID(ctx context.Context, id string) (*models.S
} }
func (s *MemoryStorage) CreateShort(ctx context.Context, short *models.Short) (*models.Short, error) { func (s *MemoryStorage) CreateShort(ctx context.Context, short *models.Short) (*models.Short, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.shortMu.Lock() s.shortMu.Lock()
@ -146,7 +156,7 @@ func (s *MemoryStorage) CreateShort(ctx context.Context, short *models.Short) (*
} }
func (s *MemoryStorage) DeleteShort(ctx context.Context, short *models.Short) error { func (s *MemoryStorage) DeleteShort(ctx context.Context, short *models.Short) error {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.shortMu.Lock() s.shortMu.Lock()
@ -169,7 +179,7 @@ func (s *MemoryStorage) DeleteShort(ctx context.Context, short *models.Short) er
} }
func (s *MemoryStorage) ListShorts(ctx context.Context, user *models.User) ([]*models.Short, error) { func (s *MemoryStorage) ListShorts(ctx context.Context, user *models.User) ([]*models.Short, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.shortIDMu.RLock() s.shortIDMu.RLock()
@ -187,7 +197,7 @@ func (s *MemoryStorage) ListShorts(ctx context.Context, user *models.User) ([]*m
} }
func (s *MemoryStorage) CreateShortLog(ctx context.Context, shortLog *models.ShortLog) error { func (s *MemoryStorage) CreateShortLog(ctx context.Context, shortLog *models.ShortLog) error {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.shortLogMu.Lock() s.shortLogMu.Lock()
@ -216,7 +226,7 @@ func (s *MemoryStorage) CreateShortLog(ctx context.Context, shortLog *models.Sho
} }
func (s *MemoryStorage) ListShortLogs(ctx context.Context, short *models.Short) ([]*models.ShortLog, error) { func (s *MemoryStorage) ListShortLogs(ctx context.Context, short *models.Short) ([]*models.ShortLog, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.shortLogMu.RLock() s.shortLogMu.RLock()
@ -231,7 +241,7 @@ func (s *MemoryStorage) ListShortLogs(ctx context.Context, short *models.Short)
} }
func (s *MemoryStorage) FindUser(ctx context.Context, username string) (*models.User, error) { func (s *MemoryStorage) FindUser(ctx context.Context, username string) (*models.User, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.userMu.RLock() s.userMu.RLock()
@ -246,7 +256,7 @@ func (s *MemoryStorage) FindUser(ctx context.Context, username string) (*models.
} }
func (s *MemoryStorage) FindUserByID(ctx context.Context, id string) (*models.User, error) { func (s *MemoryStorage) FindUserByID(ctx context.Context, id string) (*models.User, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.userIDMu.RLock() s.userIDMu.RLock()
@ -261,7 +271,7 @@ func (s *MemoryStorage) FindUserByID(ctx context.Context, id string) (*models.Us
} }
func (s *MemoryStorage) CreateUser(ctx context.Context, user *models.User) (*models.User, error) { func (s *MemoryStorage) CreateUser(ctx context.Context, user *models.User) (*models.User, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.userMu.Lock() s.userMu.Lock()
@ -293,7 +303,7 @@ func (s *MemoryStorage) CreateUser(ctx context.Context, user *models.User) (*mod
} }
func (s *MemoryStorage) DeleteUser(ctx context.Context, user *models.User) error { func (s *MemoryStorage) DeleteUser(ctx context.Context, user *models.User) error {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.userMu.Lock() s.userMu.Lock()
@ -343,7 +353,7 @@ func (s *MemoryStorage) DeleteUser(ctx context.Context, user *models.User) error
} }
func (s *MemoryStorage) FindToken(ctx context.Context, value string) (*models.Token, error) { func (s *MemoryStorage) FindToken(ctx context.Context, value string) (*models.Token, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.tokenMu.RLock() s.tokenMu.RLock()
@ -358,7 +368,7 @@ func (s *MemoryStorage) FindToken(ctx context.Context, value string) (*models.To
} }
func (s *MemoryStorage) FindTokenByID(ctx context.Context, id string) (*models.Token, error) { func (s *MemoryStorage) FindTokenByID(ctx context.Context, id string) (*models.Token, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.tokenIDMu.RLock() s.tokenIDMu.RLock()
@ -373,7 +383,7 @@ func (s *MemoryStorage) FindTokenByID(ctx context.Context, id string) (*models.T
} }
func (s *MemoryStorage) ListTokens(ctx context.Context, user *models.User) ([]*models.Token, error) { func (s *MemoryStorage) ListTokens(ctx context.Context, user *models.User) ([]*models.Token, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.tokenIDMu.RLock() s.tokenIDMu.RLock()
@ -391,7 +401,7 @@ func (s *MemoryStorage) ListTokens(ctx context.Context, user *models.User) ([]*m
} }
func (s *MemoryStorage) CreateToken(ctx context.Context, token *models.Token) (*models.Token, error) { func (s *MemoryStorage) CreateToken(ctx context.Context, token *models.Token) (*models.Token, error) {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.tokenMu.Lock() s.tokenMu.Lock()
@ -427,7 +437,7 @@ func (s *MemoryStorage) CreateToken(ctx context.Context, token *models.Token) (*
} }
func (s *MemoryStorage) DeleteToken(ctx context.Context, token *models.Token) error { func (s *MemoryStorage) DeleteToken(ctx context.Context, token *models.Token) error {
logPerf := s.logPerformance() logPerf := s.logPerformance(ctx)
defer logPerf() defer logPerf()
s.tokenMu.Lock() s.tokenMu.Lock()

View File

@ -11,26 +11,20 @@ import (
bunstorage "git.maronato.dev/maronato/goshort/internal/storage/bun" bunstorage "git.maronato.dev/maronato/goshort/internal/storage/bun"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect" "github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/sqliteshim" _ "modernc.org/sqlite"
) )
// NewSQLiteStorage creates a new SQLite storage. // NewSQLiteStorage creates a new SQLite storage.
func NewSQLiteStorage(cfg *config.Config) storage.Storage { func NewSQLiteStorage(cfg *config.Config) storage.Storage {
// Create a new SQLite database with the following pragmas enabled: if cfg.DBType == config.DBTypeMemory {
// - journal_mode=WAL: Enables Write-Ahead Logging, which allows for concurrent reads and writes. cfg.DBURL = "file::memory:?cache=shared&mode=memory&_foreign_keys=1"
// - foreign_keys=ON: Enables foreign key constraints. } else {
// - synchronous=NORMAL: Enables synchronous mode NORMAL cfg.DBURL = cfg.DBURL + "?_pragma=journal_mode=WAL&_pragma=foreign_keys=1"
sqldb, err := sql.Open(sqliteshim.ShimName, cfg.DBURL+"?_pragma=foreign_keys(ON)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)")
if err != nil {
panic(err)
} }
// If running the DB in memory, make sure sqldb, err := sql.Open("sqlite", cfg.DBURL)
// database/sql does not close idle connections. if err != nil {
// Otherwise, the database will be lost. panic(err)
if cfg.DBType == config.DBTypeMemory {
sqldb.SetMaxIdleConns(1000)
sqldb.SetConnMaxLifetime(0)
} }
db := bun.NewDB(sqldb, sqlitedialect.New()) db := bun.NewDB(sqldb, sqlitedialect.New())

View File

@ -10,6 +10,8 @@ type Storage interface {
// Lifecycle // Lifecycle
Start(ctx context.Context) error Start(ctx context.Context) error
Stop(ctx context.Context) error Stop(ctx context.Context) error
Ping(ctx context.Context) error
// Short Storage // Short Storage
// FindShort finds a short in the storage using its name. // FindShort finds a short in the storage using its name.

View File

@ -0,0 +1,49 @@
package logging
import (
"context"
"fmt"
"log/slog"
"os"
"git.maronato.dev/maronato/goshort/internal/config"
)
type logCtxKey struct{}
func NewLogger(cfg *config.Config) *slog.Logger {
level := slog.LevelInfo
addSource := false
if cfg.Debug {
level = slog.LevelDebug
addSource = true
}
if cfg.Prod {
return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
AddSource: addSource,
}))
} else {
return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
AddSource: addSource,
}))
}
}
func FromCtx(ctx context.Context) *slog.Logger {
l, ok := ctx.Value(logCtxKey{}).(*slog.Logger)
if !ok {
fmt.Println("No logger found in context")
return slog.New(NewNoOpHandler())
}
return l
}
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
return context.WithValue(ctx, logCtxKey{}, l)
}

View File

@ -0,0 +1,30 @@
package logging
import (
"context"
"log/slog"
)
type NoOpHandler struct {
slog.Handler
}
func NewNoOpHandler() slog.Handler {
return &NoOpHandler{}
}
func (h *NoOpHandler) Enabled(ctx context.Context, level slog.Level) bool {
return false
}
func (h *NoOpHandler) Handle(ctx context.Context, rec slog.Record) error {
return nil
}
func (h *NoOpHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return h
}
func (h *NoOpHandler) WithGroup(name string) slog.Handler {
return h
}