parent
16bb373c60
commit
949ea57dd9
|
@ -2,6 +2,7 @@
|
|||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
results.bin
|
||||
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
results.bin
|
||||
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
golang 1.21.0
|
|
@ -1,5 +1,5 @@
|
|||
# Load the golang image
|
||||
FROM golang:1.20.7 as go-builder
|
||||
FROM golang:1.21.0 as go-builder
|
||||
# Then the node image
|
||||
FROM node:20.5.0 as builder
|
||||
|
||||
|
@ -15,7 +15,6 @@ WORKDIR /go/src/app
|
|||
ENV GOPATH=/go
|
||||
ENV PATH=$GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
ENV GOCACHE=/tmp/.go-build-cache
|
||||
ENV CGO_ENABLED=0
|
||||
# This variable communicates to the service that it's running inside
|
||||
# a docker container.
|
||||
ENV ENV_DOCKER=true
|
||||
|
@ -51,10 +50,13 @@ WORKDIR /app
|
|||
|
||||
# Set our runtime environment
|
||||
ENV ENV_DOCKER=true
|
||||
ENV GOSHORT_PROD=true
|
||||
|
||||
COPY --from=builder /go/src/app/goshort /usr/local/bin/goshort
|
||||
|
||||
HEALTHCHECK CMD [ "goshort", "healthcheck" ]
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT [ "goshort" ]
|
||||
CMD [ "serve" ]
|
||||
|
|
2
Makefile
2
Makefile
|
@ -7,7 +7,7 @@ frontend:
|
|||
VITE_API_URL=/api npm run --prefix frontend build
|
||||
|
||||
backend:
|
||||
CGO_ENABLED=0 go build -o goshort goshort.go
|
||||
go build -o goshort goshort.go
|
||||
|
||||
all:
|
||||
make frontend
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
|
@ -16,9 +17,11 @@ import (
|
|||
servermiddleware "git.maronato.dev/maronato/goshort/internal/server/middleware"
|
||||
shortserver "git.maronato.dev/maronato/goshort/internal/server/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"
|
||||
userservice "git.maronato.dev/maronato/goshort/internal/service/user"
|
||||
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/cors"
|
||||
"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 {
|
||||
l := logging.FromCtx(ctx)
|
||||
l.Debug("Executing dev command", slog.Any("config", cfg))
|
||||
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
|
||||
// Start the API server
|
||||
|
@ -84,11 +90,16 @@ func serveAPI(ctx context.Context, cfg *config.Config) error {
|
|||
shortService := shortservice.NewShortService(storage)
|
||||
userService := userservice.NewUserService(cfg, storage)
|
||||
tokenService := tokenservice.NewTokenService(storage)
|
||||
shortLogService := shortlogservice.NewShortLogService(storage)
|
||||
|
||||
// Start short log worker
|
||||
stopWorker := shortLogService.StartWorker(ctx)
|
||||
defer stopWorker()
|
||||
|
||||
// Create handlers
|
||||
apiHandler := apiserver.NewAPIHandler(shortService, userService, tokenService)
|
||||
shortHandler := shortserver.NewShortHandler(shortService)
|
||||
healthcheckHandler := healthcheckserver.NewHealthcheckHandler()
|
||||
apiHandler := apiserver.NewAPIHandler(shortService, userService, tokenService, shortLogService)
|
||||
shortHandler := shortserver.NewShortHandler(shortService, shortLogService)
|
||||
healthcheckHandler := healthcheckserver.NewHealthcheckHandler(storage)
|
||||
|
||||
// Create routers
|
||||
apiRouter := apiserver.NewAPIRouter(apiHandler)
|
||||
|
@ -105,7 +116,7 @@ func serveAPI(ctx context.Context, cfg *config.Config) error {
|
|||
AllowedOrigins: []string{
|
||||
"http://" + net.JoinHostPort(cfg.Host, cfg.UIPort),
|
||||
},
|
||||
AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"},
|
||||
AllowedMethods: []string{"GET", "POST", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
|
||||
|
|
|
@ -4,12 +4,14 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"git.maronato.dev/maronato/goshort/cmd/shared"
|
||||
"git.maronato.dev/maronato/goshort/internal/config"
|
||||
"git.maronato.dev/maronato/goshort/internal/util/logging"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
)
|
||||
|
||||
|
@ -38,6 +40,9 @@ func New(cfg *config.Config) *ffcli.Command {
|
|||
// If the request fails, return an error
|
||||
// Otherwise, return nil.
|
||||
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{
|
||||
Host: net.JoinHostPort(cfg.Host, cfg.Port),
|
||||
Scheme: "http",
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
healthcheckcmd "git.maronato.dev/maronato/goshort/cmd/healthcheck"
|
||||
servecmd "git.maronato.dev/maronato/goshort/cmd/serve"
|
||||
"git.maronato.dev/maronato/goshort/internal/config"
|
||||
"git.maronato.dev/maronato/goshort/internal/util/logging"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
)
|
||||
|
||||
|
@ -29,6 +30,7 @@ func Run() {
|
|||
}
|
||||
|
||||
// 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" {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, devcmd.New(cfg))
|
||||
}
|
||||
|
@ -45,6 +47,10 @@ func Run() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create system logger
|
||||
l := logging.NewLogger(cfg)
|
||||
ctx = logging.WithLogger(ctx, l)
|
||||
|
||||
// Run the command.
|
||||
if err := rootCmd.Run(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.maronato.dev/maronato/goshort/cmd/shared"
|
||||
"git.maronato.dev/maronato/goshort/internal/config"
|
||||
|
@ -14,9 +15,11 @@ import (
|
|||
shortserver "git.maronato.dev/maronato/goshort/internal/server/short"
|
||||
staticssterver "git.maronato.dev/maronato/goshort/internal/server/static"
|
||||
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"
|
||||
userservice "git.maronato.dev/maronato/goshort/internal/service/user"
|
||||
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/peterbourgon/ff/v3/ffcli"
|
||||
)
|
||||
|
@ -27,6 +30,7 @@ func New(cfg *config.Config) *ffcli.Command {
|
|||
|
||||
shared.RegisterBaseFlags(fs, cfg)
|
||||
shared.RegisterServerFlags(fs, cfg)
|
||||
fs.BoolVar(&cfg.Prod, "prod", config.DefaultProd, "run in production mode")
|
||||
|
||||
// Create the command and options
|
||||
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 {
|
||||
l := logging.FromCtx(ctx)
|
||||
l.Debug("Executing serve command", slog.Any("config", cfg))
|
||||
|
||||
// Create the new server
|
||||
server := server.NewServer(cfg)
|
||||
|
||||
|
@ -58,12 +65,17 @@ func exec(ctx context.Context, cfg *config.Config) error {
|
|||
shortService := shortservice.NewShortService(storage)
|
||||
userService := userservice.NewUserService(cfg, storage)
|
||||
tokenService := tokenservice.NewTokenService(storage)
|
||||
shortLogService := shortlogservice.NewShortLogService(storage)
|
||||
|
||||
// Start short log worker
|
||||
stopWorker := shortLogService.StartWorker(ctx)
|
||||
defer stopWorker()
|
||||
|
||||
// Create handlers
|
||||
apiHandler := apiserver.NewAPIHandler(shortService, userService, tokenService)
|
||||
shortHandler := shortserver.NewShortHandler(shortService)
|
||||
apiHandler := apiserver.NewAPIHandler(shortService, userService, tokenService, shortLogService)
|
||||
shortHandler := shortserver.NewShortHandler(shortService, shortLogService)
|
||||
staticHandler := staticssterver.NewStaticHandler(cfg)
|
||||
healthcheckHandler := healthcheckserver.NewHealthcheckHandler()
|
||||
healthcheckHandler := healthcheckserver.NewHealthcheckHandler(storage)
|
||||
|
||||
// Create routers
|
||||
apiRouter := apiserver.NewAPIRouter(apiHandler)
|
||||
|
|
|
@ -41,6 +41,7 @@ func RegisterBaseFlags(fs *flag.FlagSet, cfg *config.Config) {
|
|||
fs.BoolVar(&cfg.Debug, "debug", config.DefaultDebug, "enable debug mode")
|
||||
var defaultHost = config.DefaultHost
|
||||
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"
|
||||
}
|
||||
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 {
|
||||
switch cfg.DBType {
|
||||
case config.DBTypeMemory:
|
||||
cfg.DBURL = ":memory:"
|
||||
return sqlitestorage.NewSQLiteStorage(cfg)
|
||||
case config.DBTypeSQLite:
|
||||
return sqlitestorage.NewSQLiteStorage(cfg)
|
||||
|
|
|
@ -14,12 +14,13 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-use": "^17.4.0"
|
||||
"ua-parser-js": "^1.0.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@marolint/eslint-config-react": "^1.0.2",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.47.0",
|
||||
|
@ -55,6 +56,7 @@
|
|||
"version": "7.22.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz",
|
||||
"integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
|
@ -856,11 +858,6 @@
|
|||
"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": {
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
|
||||
|
@ -911,6 +908,12 @@
|
|||
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
|
||||
"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": {
|
||||
"version": "5.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
||||
|
@ -1133,11 +1136,6 @@
|
|||
"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": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
|
@ -1668,14 +1666,6 @@
|
|||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"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": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
@ -1690,26 +1680,6 @@
|
|||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
|
@ -1725,7 +1695,8 @@
|
|||
"node_modules/csstype": {
|
||||
"version": "3.1.2",
|
||||
"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": {
|
||||
"version": "1.0.8",
|
||||
|
@ -1829,14 +1800,6 @@
|
|||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"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": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz",
|
||||
|
@ -2434,7 +2397,8 @@
|
|||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"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": {
|
||||
"version": "1.3.0",
|
||||
|
@ -2482,21 +2446,6 @@
|
|||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"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": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
|
@ -2852,11 +2801,6 @@
|
|||
"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": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
|
@ -2907,15 +2851,6 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
|
||||
|
@ -3290,11 +3225,6 @@
|
|||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -3438,11 +3368,6 @@
|
|||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
@ -3503,25 +3428,6 @@
|
|||
"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": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
|
@ -4115,45 +4021,6 @@
|
|||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -4198,7 +4065,8 @@
|
|||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"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": {
|
||||
"version": "1.5.0",
|
||||
|
@ -4217,11 +4085,6 @@
|
|||
"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": {
|
||||
"version": "1.22.4",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
|
||||
|
@ -4289,14 +4152,6 @@
|
|||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
@ -4360,17 +4215,6 @@
|
|||
"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": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
|
@ -4386,14 +4230,6 @@
|
|||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
@ -4438,14 +4274,6 @@
|
|||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
|
@ -4455,52 +4283,6 @@
|
|||
"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": {
|
||||
"version": "4.0.8",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "3.34.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
|
||||
|
@ -4733,14 +4510,6 @@
|
|||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
@ -4753,16 +4522,6 @@
|
|||
"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": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
|
@ -4784,7 +4543,8 @@
|
|||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"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": {
|
||||
"version": "3.21.0",
|
||||
|
@ -4903,6 +4663,24 @@
|
|||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||
|
|
|
@ -16,12 +16,13 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-use": "^17.4.0"
|
||||
"ua-parser-js": "^1.0.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@marolint/eslint-config-react": "^1.0.2",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"eslint": "^8.47.0",
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -10,7 +10,6 @@ const ItemList = <T extends Record<string, unknown>, K extends keyof T>({
|
|||
idKey: K
|
||||
Item: FunctionComponent<T>
|
||||
}) => {
|
||||
console.log("list")
|
||||
return (
|
||||
<>
|
||||
<ul
|
||||
|
|
|
@ -127,7 +127,7 @@ export default function Navbar() {
|
|||
isActive
|
||||
? "bg-blue-100 text-blue-600 border-blue-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}
|
||||
|
|
|
@ -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
|
|
@ -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 { UAParser } from "ua-parser-js"
|
||||
|
||||
import { useDelete } from "../hooks/useCRUD"
|
||||
import { Session } from "../types"
|
||||
import { formatDateStr } from "../util/date"
|
||||
|
||||
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 }) => {
|
||||
// Handle deletion
|
||||
const [deleting, deleteOther] = useDelete()
|
||||
|
@ -21,9 +45,54 @@ const SessionItem: FunctionComponent<Session> = ({ ...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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 { formatDateStr } from "../util/date"
|
||||
|
||||
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 [deleting, del] = useDelete()
|
||||
const doDelete = useCallback(() => del({ id: token.id }), [del, token.id])
|
||||
|
||||
const createdAt = formatDateStr(token.createdAt)
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 fetchAPI from "../util/fetchAPI"
|
||||
|
@ -73,58 +73,34 @@ export const useOnCreate = <T extends Record<string, unknown>>(
|
|||
return [creating, create] as const
|
||||
}
|
||||
|
||||
export const useCreate = <T extends SubmitTarget>(
|
||||
onCreate?: (payload?: T) => void
|
||||
) => {
|
||||
const makeUseVerb =
|
||||
(verb: Uppercase<FormMethod>) =>
|
||||
<T extends SubmitTarget>(onSubmit?: (payload?: T) => void) => {
|
||||
const submit = useSubmit()
|
||||
const navigation = useNavigation()
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const create = useCallback(
|
||||
const doSubmit = useCallback(
|
||||
(payload?: T) => {
|
||||
setCreating(true)
|
||||
setSubmitting(true)
|
||||
submit(payload ?? null, {
|
||||
method: "POST",
|
||||
method: verb,
|
||||
replace: true,
|
||||
})
|
||||
if (onCreate) onCreate(payload)
|
||||
if (onSubmit) onSubmit(payload)
|
||||
},
|
||||
[submit, onCreate]
|
||||
[submit, onSubmit]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (navigation.state === "idle") {
|
||||
setCreating(false)
|
||||
setSubmitting(false)
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
return [creating, create] as const
|
||||
return [submitting, doSubmit] as const
|
||||
}
|
||||
|
||||
export const useDelete = <T extends SubmitTarget>(
|
||||
onDelete?: (payload: T) => void
|
||||
) => {
|
||||
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
|
||||
}
|
||||
export const useCreate = makeUseVerb("POST")
|
||||
export const useDelete = makeUseVerb("DELETE")
|
||||
export const usePatch = makeUseVerb("PATCH")
|
||||
|
|
|
@ -150,7 +150,7 @@ export const useVisitMetrics = (logs: ShortLog[]) => {
|
|||
let last = logs[0].createdAt
|
||||
logs.forEach((log) => {
|
||||
// If the log is older, update
|
||||
if (log.createdAt.localeCompare(last) < 0) {
|
||||
if (log.createdAt.localeCompare(last) > 0) {
|
||||
last = log.createdAt
|
||||
}
|
||||
// Add IP to set
|
||||
|
|
|
@ -187,7 +187,7 @@ export const Component: FunctionComponent = () => {
|
|||
Choose a{" "}
|
||||
<span className="text-green-500 font-bold">custom link</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">
|
||||
{`${location.host}/`}
|
||||
</span>
|
||||
|
@ -212,9 +212,11 @@ export const Component: FunctionComponent = () => {
|
|||
{shortening ? "Shortening..." : "Shorten it"}
|
||||
</Button>
|
||||
</form>
|
||||
{recentShorts.length ? (
|
||||
<div className="mt-10">
|
||||
<ItemList items={recentShorts} Item={ShortItem} idKey="id" />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ export function Component() {
|
|||
maxLength={128}
|
||||
required
|
||||
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>
|
||||
<Button
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { FunctionComponent, useCallback } from "react"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { LoaderFunction, redirect } from "react-router-dom"
|
||||
|
||||
import Header from "../components/Header"
|
||||
import ItemList from "../components/ItemList"
|
||||
import NoItems from "../components/NoItems"
|
||||
import SessionItem from "../components/SessionItem"
|
||||
import { protectedLoader } from "../hooks/useAuth"
|
||||
import { useSortedLoadedItems } from "../hooks/useLoaderItems"
|
||||
|
@ -17,14 +16,10 @@ export function Component() {
|
|||
useCallback((a, b) => b.lastActivity.localeCompare(a.lastActivity), [])
|
||||
)
|
||||
|
||||
const Sessions: FunctionComponent = () => {
|
||||
return <ItemList items={items} Item={SessionItem} idKey="id" />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Sessions" />
|
||||
{items.length > 0 ? <Sessions /> : <NoItems type="Session" />}
|
||||
<ItemList items={items} Item={SessionItem} idKey="id" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ export function Component() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<BackButton className="absolute mt-10" />
|
||||
<BackButton className="absolute mt-5" />
|
||||
<Header title={short.name} />
|
||||
{short.url}
|
||||
</>
|
||||
|
|
|
@ -55,7 +55,7 @@ export function Component() {
|
|||
maxLength={128}
|
||||
required
|
||||
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>
|
||||
<Button
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FunctionComponent, useCallback } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
|
||||
import { PlusIcon } from "@heroicons/react/24/outline"
|
||||
import { LoaderFunction, redirect } from "react-router-dom"
|
||||
|
@ -18,19 +18,36 @@ export function Component() {
|
|||
const [items] = useSortedLoadedItems<Token>(
|
||||
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
|
||||
const [creating, create] = useCreate()
|
||||
const onClickCreate = useCallback(() => create(), [create])
|
||||
|
||||
const Tokens: FunctionComponent = () => {
|
||||
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>
|
||||
const onClickCreate = useCallback(
|
||||
() => create({ name: generateName() }),
|
||||
[create, generateName]
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -42,7 +59,7 @@ export function Component() {
|
|||
<PlusIcon className="inline-block w-5 h-5 mb-0.5 mr-1 leading-1" />
|
||||
<span>New token</span>
|
||||
</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({
|
||||
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) => {
|
||||
const id = formData.get("id") as string | null
|
||||
const name = formData.get("name") as string | null
|
||||
|
|
|
@ -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
16
go.mod
|
@ -1,6 +1,6 @@
|
|||
module git.maronato.dev/maronato/goshort
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/alexedwards/scs/v2 v2.5.1
|
||||
|
@ -10,10 +10,10 @@ require (
|
|||
github.com/peterbourgon/ff/v3 v3.4.0
|
||||
github.com/uptrace/bun 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
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/sync v0.3.0
|
||||
modernc.org/sqlite v1.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -25,22 +25,22 @@ require (
|
|||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // 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/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // 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/tools v0.9.1 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // 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/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.24.1 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.6.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sqlite v1.25.0 // 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
51
go.sum
|
@ -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/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/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/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
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/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
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/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
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/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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
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.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
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/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
|
||||
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/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/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/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
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/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/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/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/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/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/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
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/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||
golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
|
||||
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
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/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
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/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/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
|
||||
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
||||
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/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
|
||||
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/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
|
||||
|
|
|
@ -18,12 +18,13 @@ const (
|
|||
var DBTypes = [...]string{
|
||||
DBTypeMemory,
|
||||
DBTypeSQLite,
|
||||
DBTypePostgres,
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultProd is the default mode.
|
||||
DefaultProd = false
|
||||
// DefaultDBType is the default type of database to use.
|
||||
DefaultDBType = DBTypeMemory
|
||||
DefaultDBType = DBTypeSQLite
|
||||
// DefaultDBURL is the default connection string for the database.
|
||||
DefaultDBURL = "goshort.db"
|
||||
// DefaultPort is the default port to listen on.
|
||||
|
|
|
@ -3,6 +3,8 @@ package apiserver
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
|
@ -10,9 +12,11 @@ import (
|
|||
"git.maronato.dev/maronato/goshort/internal/server"
|
||||
servermiddleware "git.maronato.dev/maronato/goshort/internal/server/middleware"
|
||||
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"
|
||||
userservice "git.maronato.dev/maronato/goshort/internal/service/user"
|
||||
"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/render"
|
||||
)
|
||||
|
@ -21,14 +25,16 @@ type APIHandler struct {
|
|||
shorts *shortservice.ShortService
|
||||
users *userservice.UserService
|
||||
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{
|
||||
shorts: shorts,
|
||||
users: users,
|
||||
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) {
|
||||
ctx := r.Context()
|
||||
l := logging.FromCtx(ctx)
|
||||
|
||||
type loginForm struct {
|
||||
Username string `json:"username"`
|
||||
|
@ -93,6 +100,8 @@ func (h *APIHandler) Login(w http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
if errors.Is(err, errs.ErrUserDoesNotExist) || errors.Is(err, errs.ErrFailedAuthentication) {
|
||||
// If the username or password are wrong, return invalid username/password
|
||||
l.Debug("failed to authenticate user", "err", err)
|
||||
|
||||
server.RenderUnauthorized(w, r)
|
||||
} else if errors.Is(err, errs.ErrInvalidUser) {
|
||||
// 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) {
|
||||
ctx := r.Context()
|
||||
l := logging.FromCtx(ctx)
|
||||
|
||||
// Get the user from the json body
|
||||
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) {
|
||||
server.RenderBadRequest(w, r, err)
|
||||
} else if errors.Is(err, errs.ErrRegistrationDisabled) {
|
||||
l.Debug("failed to create user", "err", err)
|
||||
|
||||
server.RenderForbidden(w, r)
|
||||
} else {
|
||||
server.RenderServerError(w, r, err)
|
||||
|
@ -293,7 +305,7 @@ func (h *APIHandler) ListShortLogs(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// Get logs
|
||||
logs, err := h.shorts.ListLogs(ctx, short)
|
||||
logs, err := h.shortLogs.ListLogs(ctx, short)
|
||||
if err != nil {
|
||||
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) {
|
||||
ctx := r.Context()
|
||||
l := logging.FromCtx(ctx)
|
||||
|
||||
// Get user from context
|
||||
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)
|
||||
if err != nil {
|
||||
if errors.Is(err, errs.ErrSessionDoesNotExist) {
|
||||
l.Debug("could not delete session", "err", err)
|
||||
|
||||
server.RenderNotFound(w, r)
|
||||
} else {
|
||||
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) {
|
||||
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
|
||||
user, ok := h.findUserOrRespond(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.tokens.CreateToken(ctx, user)
|
||||
token, err := h.tokens.CreateToken(ctx, user, form.Name)
|
||||
if err != nil {
|
||||
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.
|
||||
func (h *APIHandler) findShortOrRespond(w http.ResponseWriter, r *http.Request) (short *models.Short, ok bool) {
|
||||
ctx := r.Context()
|
||||
l := logging.FromCtx(ctx)
|
||||
|
||||
// Get short id from request
|
||||
id := chi.URLParam(r, "id")
|
||||
|
@ -481,6 +509,8 @@ func (h *APIHandler) findShortOrRespond(w http.ResponseWriter, r *http.Request)
|
|||
if err != nil {
|
||||
// If the short doesn't exist or is invalid, return not found
|
||||
if errors.Is(err, errs.ErrShortDoesNotExist) || errors.Is(err, errs.ErrInvalidShort) {
|
||||
l.Debug("could not find short", "err", err)
|
||||
|
||||
server.RenderNotFound(w, r)
|
||||
} else {
|
||||
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,
|
||||
// return forbidden.
|
||||
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)
|
||||
|
||||
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.
|
||||
func (h *APIHandler) findTokenOrRespond(w http.ResponseWriter, r *http.Request) (token *models.Token, ok bool) {
|
||||
ctx := r.Context()
|
||||
l := logging.FromCtx(ctx)
|
||||
|
||||
// Get token ID from request
|
||||
id := chi.URLParam(r, "id")
|
||||
|
@ -520,6 +553,8 @@ func (h *APIHandler) findTokenOrRespond(w http.ResponseWriter, r *http.Request)
|
|||
if err != nil {
|
||||
// If the token doesn't exist or is invalid, return not found
|
||||
if errors.Is(err, errs.ErrTokenDoesNotExist) || errors.Is(err, errs.ErrInvalidToken) {
|
||||
l.Debug("could not find token", "err", err)
|
||||
|
||||
server.RenderNotFound(w, r)
|
||||
} else {
|
||||
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,
|
||||
// return NotFound.
|
||||
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)
|
||||
|
||||
return nil, false
|
||||
|
|
|
@ -3,6 +3,7 @@ package devuiserver
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -10,6 +11,7 @@ import (
|
|||
"syscall"
|
||||
|
||||
"git.maronato.dev/maronato/goshort/internal/config"
|
||||
"git.maronato.dev/maronato/goshort/internal/util/logging"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
|
@ -41,8 +43,11 @@ func NewServer(cfg *config.Config) *Server {
|
|||
}
|
||||
|
||||
func (s *Server) ListenAndServe(ctx context.Context) error {
|
||||
l := logging.FromCtx(ctx)
|
||||
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
l.Info("Starting UI dev server", slog.String("addr", net.JoinHostPort(s.host, s.uiPort)))
|
||||
|
||||
return s.Start(egCtx)
|
||||
})
|
||||
|
@ -50,6 +55,8 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
|
|||
// Wait for the context to be done
|
||||
<-egCtx.Done()
|
||||
// Shutdown the server
|
||||
l.Info("Shutting down UI dev server")
|
||||
|
||||
return s.Shutdown()
|
||||
})
|
||||
|
||||
|
@ -61,6 +68,8 @@ func (s *Server) ListenAndServe(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
|
||||
uiCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
@ -109,6 +118,8 @@ func (s *Server) Start(ctx context.Context) error {
|
|||
return fmt.Errorf("error killing process group: %w", err)
|
||||
}
|
||||
|
||||
l.Info("UI dev server shutdown complete")
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
|
|
|
@ -43,10 +43,6 @@ func ErrBadRequest(err error) render.Renderer {
|
|||
return ErrGeneric(err, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func ErrServerError(err error) render.Renderer {
|
||||
return ErrGeneric(err, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func ErrNotFound() render.Renderer {
|
||||
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) {
|
||||
RenderRender(w, r, ErrServerError(err))
|
||||
// Panic so the stack trace is printed
|
||||
panic(err)
|
||||
}
|
||||
|
||||
func RenderNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -3,32 +3,74 @@ package healthcheckserver
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"git.maronato.dev/maronato/goshort/internal/storage"
|
||||
"git.maronato.dev/maronato/goshort/internal/util/logging"
|
||||
"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) {
|
||||
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
|
||||
response := &healthCheckResponse{
|
||||
Status: "ok",
|
||||
ServerOK: true,
|
||||
DatabaseOK: true,
|
||||
Status: overallStatus,
|
||||
ServerOK: serverOk,
|
||||
DatabaseOK: databaseOk,
|
||||
}
|
||||
|
||||
// Render the response
|
||||
render.Status(r, http.StatusOK)
|
||||
render.Status(r, statusCode)
|
||||
render.JSON(w, r, response)
|
||||
}
|
||||
|
||||
type healthCheckResponse struct {
|
||||
// Status is the status of the health check
|
||||
Status string `json:"status"`
|
||||
Status HealthCheckStatus `json:"status"`
|
||||
// ServerOK is the status of the server
|
||||
ServerOK bool `json:"server_ok"`
|
||||
// DatabaseOK is the status of the database
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -20,7 +20,7 @@ func SessionManager(cfg *config.Config) func(http.Handler) http.Handler {
|
|||
Path: "/",
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Persist: true,
|
||||
// Secure: cfg.Prod,
|
||||
Secure: cfg.Prod,
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
|
|
|
@ -3,11 +3,13 @@ package server
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"git.maronato.dev/maronato/goshort/internal/config"
|
||||
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/middleware"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
@ -26,10 +28,11 @@ func NewServer(cfg *config.Config) *Server {
|
|||
mux := chi.NewRouter()
|
||||
|
||||
// Register default middlewares
|
||||
mux.Use(middleware.Recoverer)
|
||||
requestLogger := servermiddleware.NewLogFormatter()
|
||||
mux.Use(middleware.RequestID)
|
||||
mux.Use(middleware.RealIP)
|
||||
mux.Use(middleware.Logger)
|
||||
mux.Use(middleware.RequestLogger(requestLogger))
|
||||
mux.Use(middleware.Recoverer)
|
||||
mux.Use(servermiddleware.SessionManager(cfg))
|
||||
mux.Use(middleware.Timeout(config.RequestTimeout))
|
||||
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 {
|
||||
l := logging.FromCtx(ctx)
|
||||
|
||||
// Create the errorgroup that will manage the server execution
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
|
||||
// Start the server
|
||||
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()
|
||||
})
|
||||
|
@ -64,11 +75,17 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
|
|||
// Wait for the context to be done
|
||||
<-egCtx.Done()
|
||||
|
||||
l.Info("Shutting down server")
|
||||
|
||||
return s.srv.Shutdown(
|
||||
context.Background(),
|
||||
)
|
||||
})
|
||||
|
||||
s.srv.RegisterOnShutdown(func() {
|
||||
l.Info("Server shutdown complete")
|
||||
})
|
||||
|
||||
// Ignore the error if the context was canceled
|
||||
if err := eg.Wait(); err != nil && ctx.Err() == nil {
|
||||
return fmt.Errorf("server exited with error: %w", err)
|
||||
|
|
|
@ -2,23 +2,25 @@ package shortserver
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.maronato.dev/maronato/goshort/internal/errs"
|
||||
"git.maronato.dev/maronato/goshort/internal/server"
|
||||
shortservice "git.maronato.dev/maronato/goshort/internal/service/short"
|
||||
shortlogservice "git.maronato.dev/maronato/goshort/internal/service/shortlog"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
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{
|
||||
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")
|
||||
|
||||
// Get the URL from the service
|
||||
short, err := h.service.FindShort(ctx, name)
|
||||
short, err := h.shorts.FindShort(ctx, name)
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
|
||||
// If there's no error, log the access and redirect to the URL
|
||||
err = h.service.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)
|
||||
}
|
||||
h.shortLogs.LogShortAccess(ctx, short, r)
|
||||
|
||||
http.Redirect(w, r, short.URL, http.StatusSeeOther)
|
||||
case errors.Is(err, errs.ErrInvalidShort):
|
||||
|
||||
// If the short name is invalid, do nothing and let the static handler
|
||||
// take care of it.
|
||||
case errors.Is(err, errs.ErrShortDoesNotExist):
|
||||
|
||||
// If the short doesn't exist, do nothing and let the static handler
|
||||
// take care of it.
|
||||
default:
|
||||
|
||||
// Oops, this shouldn't happen.
|
||||
server.RenderRender(w, r, server.ErrServerError(err))
|
||||
server.RenderServerError(w, r, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
|
@ -33,33 +32,6 @@ func NewShortService(db storage.Storage) *ShortService {
|
|||
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) {
|
||||
// Check if the short is valid
|
||||
err := ShortNameIsValid(name)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -15,7 +15,7 @@ const (
|
|||
// DefaultTokenLength is the default length of a token.
|
||||
TokenLength = 32
|
||||
// TokenPrefix is the prefix of a token.
|
||||
TokenPrefix = "goshort-token:"
|
||||
TokenPrefix = "gst_"
|
||||
// DefaultTokenIDLength is the default length of a token ID.
|
||||
TokenIDLength = 16
|
||||
// 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.
|
||||
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
|
||||
token := &models.Token{
|
||||
Name: fmt.Sprintf("%s's token", user.Username),
|
||||
Name: name,
|
||||
Value: TokenPrefix + tokenutil.GenerateSecureToken(TokenLength/2),
|
||||
UserID: &user.ID,
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ type ShortModel struct {
|
|||
// CreatedAt is when the short was created (initialized by the storage)
|
||||
CreatedAt time.Time `bun:",notnull,default:current_timestamp" json:"createdAt"`
|
||||
// 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
|
||||
// This can be null if the short was deleted
|
||||
|
|
|
@ -3,6 +3,7 @@ package bunstorage
|
|||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.maronato.dev/maronato/goshort/internal/config"
|
||||
|
@ -143,6 +144,10 @@ func (s *BunStorage) Stop(ctx context.Context) error {
|
|||
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) {
|
||||
shortModel := new(ShortModel)
|
||||
|
||||
|
@ -199,6 +204,10 @@ func (s *BunStorage) CreateShort(ctx context.Context, short *models.Short) (*mod
|
|||
Model(shortModel).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
err = errs.ErrUserExists
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
shorts := []*models.Short{}
|
||||
for _, shortModel := range shortModels {
|
||||
shorts = append(shorts, shortModel.toShort())
|
||||
shorts := make([]*models.Short, len(shortModels))
|
||||
for i, shortModel := range shortModels {
|
||||
shorts[i] = shortModel.toShort()
|
||||
}
|
||||
return shorts, nil
|
||||
}
|
||||
|
@ -269,6 +278,10 @@ func (s *BunStorage) CreateShortLog(ctx context.Context, shortLog *models.ShortL
|
|||
Model(shortLogModel).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
err = errs.ErrUserExists
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
shortLogs := []*models.ShortLog{}
|
||||
for _, shortLogModel := range shortLogModels {
|
||||
shortLogs = append(shortLogs, shortLogModel.toShortLog())
|
||||
shortLogs := make([]*models.ShortLog, len(shortLogModels))
|
||||
for i, shortLogModel := range shortLogModels {
|
||||
shortLogs[i] = shortLogModel.toShortLog()
|
||||
}
|
||||
return shortLogs, nil
|
||||
}
|
||||
|
@ -330,6 +343,10 @@ func (s *BunStorage) CreateUser(ctx context.Context, user *models.User) (*models
|
|||
Model(userModel).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
err = errs.ErrUserExists
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
tokens := []*models.Token{}
|
||||
for _, tokenModel := range tokenModels {
|
||||
tokens = append(tokens, tokenModel.toToken())
|
||||
tokens := make([]*models.Token, len(tokenModels))
|
||||
for i, tokenModel := range tokenModels {
|
||||
tokens[i] = tokenModel.toToken()
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
|
@ -435,6 +452,10 @@ func (s *BunStorage) CreateToken(ctx context.Context, token *models.Token) (*mod
|
|||
Model(tokenModel).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
|
||||
err = errs.ErrUserExists
|
||||
}
|
||||
|
||||
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)).
|
||||
Set("name = ?", name).
|
||||
Where("id = ?", token.ID).
|
||||
Returning("*").
|
||||
Exec(ctx, newToken)
|
||||
if err != nil {
|
||||
return nil, errs.Errorf("failed to change token name", err)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"git.maronato.dev/maronato/goshort/internal/errs"
|
||||
"git.maronato.dev/maronato/goshort/internal/storage"
|
||||
"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.
|
||||
|
@ -48,26 +49,28 @@ func NewMemoryStorage(cfg *config.Config) *MemoryStorage {
|
|||
}
|
||||
|
||||
// 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()
|
||||
|
||||
return func() {
|
||||
elapsed := time.Since(start)
|
||||
took := time.Since(start)
|
||||
if s.debug {
|
||||
l := logging.FromCtx(ctx)
|
||||
|
||||
pc, _, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
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.
|
||||
func (s *MemoryStorage) Start(ctx context.Context) error {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
return nil
|
||||
|
@ -75,7 +78,14 @@ func (s *MemoryStorage) Start(ctx context.Context) error {
|
|||
|
||||
// Stop stops the storage.
|
||||
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()
|
||||
|
||||
return nil
|
||||
|
@ -83,7 +93,7 @@ func (s *MemoryStorage) Stop(ctx context.Context) error {
|
|||
|
||||
// FindShort finds a short in the storage.
|
||||
func (s *MemoryStorage) FindShort(ctx context.Context, name string) (*models.Short, error) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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.
|
||||
func (s *MemoryStorage) FindShortByID(ctx context.Context, id string) (*models.Short, error) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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 {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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 {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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 {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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) {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
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 {
|
||||
logPerf := s.logPerformance()
|
||||
logPerf := s.logPerformance(ctx)
|
||||
defer logPerf()
|
||||
|
||||
s.tokenMu.Lock()
|
||||
|
|
|
@ -11,26 +11,20 @@ import (
|
|||
bunstorage "git.maronato.dev/maronato/goshort/internal/storage/bun"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"github.com/uptrace/bun/driver/sqliteshim"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// NewSQLiteStorage creates a new SQLite storage.
|
||||
func NewSQLiteStorage(cfg *config.Config) storage.Storage {
|
||||
// Create a new SQLite database with the following pragmas enabled:
|
||||
// - journal_mode=WAL: Enables Write-Ahead Logging, which allows for concurrent reads and writes.
|
||||
// - foreign_keys=ON: Enables foreign key constraints.
|
||||
// - synchronous=NORMAL: Enables synchronous mode NORMAL
|
||||
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 cfg.DBType == config.DBTypeMemory {
|
||||
cfg.DBURL = "file::memory:?cache=shared&mode=memory&_foreign_keys=1"
|
||||
} else {
|
||||
cfg.DBURL = cfg.DBURL + "?_pragma=journal_mode=WAL&_pragma=foreign_keys=1"
|
||||
}
|
||||
|
||||
// If running the DB in memory, make sure
|
||||
// database/sql does not close idle connections.
|
||||
// Otherwise, the database will be lost.
|
||||
if cfg.DBType == config.DBTypeMemory {
|
||||
sqldb.SetMaxIdleConns(1000)
|
||||
sqldb.SetConnMaxLifetime(0)
|
||||
sqldb, err := sql.Open("sqlite", cfg.DBURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
db := bun.NewDB(sqldb, sqlitedialect.New())
|
||||
|
|
|
@ -10,6 +10,8 @@ type Storage interface {
|
|||
// Lifecycle
|
||||
Start(ctx context.Context) error
|
||||
Stop(ctx context.Context) error
|
||||
Ping(ctx context.Context) error
|
||||
|
||||
// Short Storage
|
||||
|
||||
// FindShort finds a short in the storage using its name.
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user