feat: Add comprehensive backend validation, toast notifications, and Tokyo Night theme

- Backend: Add server-side validation with go-playground/validator/v10
- Frontend: Add toast notifications for API errors with @zerodevx/svelte-toast
- UI: Complete Tokyo Night theme redesign with modern animations
- Docs: Update COMPLETE_DOCUMENTATION.md and README.md with all enhancements
- Validation: Email format, password strength, and input sanitization
- UX: Real-time error feedback, loading states, and responsive design
This commit is contained in:
john-okeefe committed 2026-01-21 20:01:18 -05:00
1 parent 7448dfff30
commit 55c42f1f99
84 files changed
+7749 -4

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
.git
.gitignore
README.md
*.md
.env
.DS_Store
.vscode
.idea
tmp/
logs/
+58
View File
@@ -0,0 +1,58 @@
# Build stage
FROM golang:1.25-alpine AS builder
WORKDIR /app
# Install Node.js and npm
RUN apk add --no-cache nodejs npm
# Install sqlc
RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# Copy backend source code
COPY backend/ ./backend/
# Copy frontend source for building
COPY frontend/package*.json ./frontend/
WORKDIR /app/frontend
# Install frontend dependencies (cached if package.json unchanged)
RUN npm ci
# Copy frontend source
COPY frontend/ ./
# Build frontend
RUN npm run build
# Go back to root
WORKDIR /app
# Generate sqlc code
RUN cd backend && sqlc generate
# Build the application
RUN cd backend && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o ../main ./cmd/server
# Final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copy the binary from builder stage
COPY --from=builder /app/main .
# Copy migrations (if needed for initialization)
COPY --from=builder /app/backend/migrations ./migrations
# Copy static files
COPY --from=builder /app/frontend/build ./static
# Expose port
EXPOSE 8080
# Run the binary
CMD ["./main"]
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"bookmann/internal/config"
"bookmann/internal/database"
"bookmann/internal/handlers"
"log"
"github.com/go-playground/validator/v10"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// CustomValidator wraps the go-playground validator
type CustomValidator struct {
validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.validator.Struct(i)
}
func main() {
cfg := config.LoadConfig()
pool, err := database.NewConnection(cfg.DatabaseURL())
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
defer pool.Close()
queries := database.New(pool)
e := echo.New()
// Set up validator
e.Validator = &CustomValidator{validator: validator.New()}
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Auth routes (no auth required)
auth := handlers.NewAuthHandler(queries, cfg.JWTSecret)
e.POST("/api/auth/register", auth.Register)
e.POST("/api/auth/login", auth.Login)
// JWT middleware for protected routes
jwtMiddleware := middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte(cfg.JWTSecret),
ContextKey: "user",
SuccessHandler: func(c echo.Context) {
token := c.Get("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
c.Set("user_id", claims["user_id"])
},
})
// Protected routes
protected := e.Group("/api", jwtMiddleware)
protected.GET("/auth/profile", auth.GetProfile)
// Routes
handlers.SetupRoutes(protected, queries)
// Serve static files
e.Static("/", "static")
// Start server
log.Printf("Starting server on port %s", cfg.ServerPort)
e.Logger.Fatal(e.Start(":" + cfg.ServerPort))
}
+33
View File
@@ -0,0 +1,33 @@
module bookmann
go 1.25
require (
github.com/go-playground/validator/v10 v10.30.1
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.4.0
github.com/jackc/pgx/v5 v5.4.3
github.com/labstack/echo/v4 v4.11.3
golang.org/x/crypto v0.46.0
)
require (
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.3.0 // indirect
)
+74
View File
@@ -0,0 +1,74 @@
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/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+42
View File
@@ -0,0 +1,42 @@
package config
import (
"fmt"
"os"
)
type Config struct {
ServerPort string
JWTSecret string
UploadPath string
DatabaseHost string
DatabasePort string
DatabaseUser string
DatabasePassword string
DatabaseName string
}
func LoadConfig() *Config {
return &Config{
ServerPort: getEnv("SERVER_PORT", "8080"),
DatabaseHost: getEnv("DATABASE_HOST", "localhost"),
DatabasePort: getEnv("DATABASE_PORT", "5432"),
DatabaseUser: getEnv("DATABASE_USER", "postgres"),
DatabasePassword: getEnv("DATABASE_PASSWORD", "password"),
DatabaseName: getEnv("DATABASE_NAME", "ebookdb"),
JWTSecret: getEnv("JWT_SECRET", "your-secret-key"),
UploadPath: getEnv("UPLOAD_PATH", "./uploads"),
}
}
func (c *Config) DatabaseURL() string {
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
c.DatabaseUser, c.DatabasePassword, c.DatabaseHost, c.DatabasePort, c.DatabaseName)
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
+20
View File
@@ -0,0 +1,20 @@
package database
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
func NewConnection(databaseURL string) (*pgxpool.Pool, error) {
pool, err := pgxpool.New(context.Background(), databaseURL)
if err != nil {
return nil, err
}
if err := pool.Ping(context.Background()); err != nil {
return nil, err
}
return pool, nil
}
+32
View File
@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}
+41
View File
@@ -0,0 +1,41 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Ebooks struct {
ID pgtype.UUID `db:"id" json:"id"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type ReadingProgress struct {
ID pgtype.UUID `db:"id" json:"id"`
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"`
TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"`
LastReadAt pgtype.Timestamptz `db:"last_read_at" json:"last_read_at"`
}
type Users struct {
ID pgtype.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
PasswordHash string `db:"password_hash" json:"password_hash"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
type Querier interface {
CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error)
CreateUser(ctx context.Context, arg CreateUserParams) (Users, error)
DeleteEbook(ctx context.Context, id pgtype.UUID) error
DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error
GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error)
GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error)
GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error)
GetUserByEmail(ctx context.Context, email string) (Users, error)
GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error)
GetUserByUsername(ctx context.Context, username string) (Users, error)
ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebooks, error)
UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error)
UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error)
}
var _ Querier = (*Queries)(nil)
+356
View File
@@ -0,0 +1,356 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: queries.sql
package database
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CreateEbook = `-- name: CreateEbook :one
INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at
`
type CreateEbookParams struct {
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
}
func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error) {
row := q.db.QueryRow(ctx, CreateEbook,
arg.Title,
arg.Author,
arg.Isbn,
arg.Description,
arg.FilePath,
arg.FileSize,
arg.MimeType,
arg.CoverImagePath,
)
var i Ebooks
err := row.Scan(
&i.ID,
&i.Title,
&i.Author,
&i.Isbn,
&i.Description,
&i.FilePath,
&i.FileSize,
&i.MimeType,
&i.CoverImagePath,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateUser = `-- name: CreateUser :one
INSERT INTO users (email, username, password_hash)
VALUES ($1, $2, $3)
RETURNING id, email, username, password_hash, created_at, updated_at
`
type CreateUserParams struct {
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
PasswordHash string `db:"password_hash" json:"password_hash"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (Users, error) {
row := q.db.QueryRow(ctx, CreateUser, arg.Email, arg.Username, arg.PasswordHash)
var i Users
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const DeleteEbook = `-- name: DeleteEbook :exec
DELETE FROM ebooks WHERE id = $1
`
func (q *Queries) DeleteEbook(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, DeleteEbook, id)
return err
}
const DeleteReadingProgress = `-- name: DeleteReadingProgress :exec
DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2
`
type DeleteReadingProgressParams struct {
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
}
func (q *Queries) DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error {
_, err := q.db.Exec(ctx, DeleteReadingProgress, arg.EbookID, arg.UserID)
return err
}
const GetEbook = `-- name: GetEbook :one
SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at FROM ebooks WHERE id = $1
`
func (q *Queries) GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error) {
row := q.db.QueryRow(ctx, GetEbook, id)
var i Ebooks
err := row.Scan(
&i.ID,
&i.Title,
&i.Author,
&i.Isbn,
&i.Description,
&i.FilePath,
&i.FileSize,
&i.MimeType,
&i.CoverImagePath,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetReadingProgress = `-- name: GetReadingProgress :one
SELECT id, ebook_id, user_id, current_page, total_pages, last_read_at FROM reading_progress WHERE ebook_id = $1 AND user_id = $2
`
type GetReadingProgressParams struct {
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
}
func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error) {
row := q.db.QueryRow(ctx, GetReadingProgress, arg.EbookID, arg.UserID)
var i ReadingProgress
err := row.Scan(
&i.ID,
&i.EbookID,
&i.UserID,
&i.CurrentPage,
&i.TotalPages,
&i.LastReadAt,
)
return i, err
}
const GetUser = `-- name: GetUser :one
SELECT id, email, username, created_at, updated_at FROM users WHERE id = $1
`
type GetUserRow struct {
ID pgtype.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error) {
row := q.db.QueryRow(ctx, GetUser, id)
var i GetUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetUserByEmail = `-- name: GetUserByEmail :one
SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE email = $1
`
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, error) {
row := q.db.QueryRow(ctx, GetUserByEmail, email)
var i Users
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE email = $1 OR username = $1
`
func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) {
row := q.db.QueryRow(ctx, GetUserByEmailOrUsername, email)
var i Users
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetUserByUsername = `-- name: GetUserByUsername :one
SELECT id, email, username, password_hash, created_at, updated_at FROM users WHERE username = $1
`
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users, error) {
row := q.db.QueryRow(ctx, GetUserByUsername, username)
var i Users
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const ListEbooks = `-- name: ListEbooks :many
SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2
`
type ListEbooksParams struct {
Limit int32 `db:"limit" json:"limit"`
Offset int32 `db:"offset" json:"offset"`
}
func (q *Queries) ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebooks, error) {
rows, err := q.db.Query(ctx, ListEbooks, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Ebooks{}
for rows.Next() {
var i Ebooks
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Author,
&i.Isbn,
&i.Description,
&i.FilePath,
&i.FileSize,
&i.MimeType,
&i.CoverImagePath,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateEbook = `-- name: UpdateEbook :one
UPDATE ebooks SET
title = $2,
author = $3,
isbn = $4,
description = $5,
cover_image_path = $6,
updated_at = NOW()
WHERE id = $1
RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, created_at, updated_at
`
type UpdateEbookParams struct {
ID pgtype.UUID `db:"id" json:"id"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
}
func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error) {
row := q.db.QueryRow(ctx, UpdateEbook,
arg.ID,
arg.Title,
arg.Author,
arg.Isbn,
arg.Description,
arg.CoverImagePath,
)
var i Ebooks
err := row.Scan(
&i.ID,
&i.Title,
&i.Author,
&i.Isbn,
&i.Description,
&i.FilePath,
&i.FileSize,
&i.MimeType,
&i.CoverImagePath,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const UpdateReadingProgress = `-- name: UpdateReadingProgress :one
INSERT INTO reading_progress (ebook_id, user_id, current_page, total_pages, last_read_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (ebook_id, user_id)
DO UPDATE SET
current_page = EXCLUDED.current_page,
total_pages = EXCLUDED.total_pages,
last_read_at = NOW()
RETURNING id, ebook_id, user_id, current_page, total_pages, last_read_at
`
type UpdateReadingProgressParams struct {
EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"`
TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"`
}
func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error) {
row := q.db.QueryRow(ctx, UpdateReadingProgress,
arg.EbookID,
arg.UserID,
arg.CurrentPage,
arg.TotalPages,
)
var i ReadingProgress
err := row.Scan(
&i.ID,
&i.EbookID,
&i.UserID,
&i.CurrentPage,
&i.TotalPages,
&i.LastReadAt,
)
return i, err
}
@@ -0,0 +1,57 @@
-- name: CreateUser :one
INSERT INTO users (email, username, password_hash)
VALUES ($1, $2, $3)
RETURNING *;
-- name: GetUserByEmail :one
SELECT * FROM users WHERE email = $1;
-- name: GetUserByUsername :one
SELECT * FROM users WHERE username = $1;
-- name: GetUserByEmailOrUsername :one
SELECT * FROM users WHERE email = $1 OR username = $1;
-- name: GetUser :one
SELECT id, email, username, created_at, updated_at FROM users WHERE id = $1;
-- name: GetEbook :one
SELECT * FROM ebooks WHERE id = $1;
-- name: ListEbooks :many
SELECT * FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2;
-- name: CreateEbook :one
INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: UpdateEbook :one
UPDATE ebooks SET
title = $2,
author = $3,
isbn = $4,
description = $5,
cover_image_path = $6,
updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: DeleteEbook :exec
DELETE FROM ebooks WHERE id = $1;
-- name: GetReadingProgress :one
SELECT * FROM reading_progress WHERE ebook_id = $1 AND user_id = $2;
-- name: UpdateReadingProgress :one
INSERT INTO reading_progress (ebook_id, user_id, current_page, total_pages, last_read_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (ebook_id, user_id)
DO UPDATE SET
current_page = EXCLUDED.current_page,
total_pages = EXCLUDED.total_pages,
last_read_at = NOW()
RETURNING *;
-- name: DeleteReadingProgress :exec
DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2;
+166
View File
@@ -0,0 +1,166 @@
package handlers
import (
"bookmann/internal/database"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
)
type AuthHandler struct {
db *database.Queries
jwtKey []byte
}
func NewAuthHandler(db *database.Queries, jwtSecret string) *AuthHandler {
return &AuthHandler{
db: db,
jwtKey: []byte(jwtSecret),
}
}
type RegisterRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=50"`
Password string `json:"password" validate:"required,min=6"`
}
type LoginRequest struct {
Login string `json:"login" validate:"required"` // email or username
Password string `json:"password" validate:"required"`
}
type AuthResponse struct {
Token string `json:"token"`
User UserProfile `json:"user"`
}
type UserProfile struct {
ID string `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
}
// Register handles POST /api/auth/register
func (h *AuthHandler) Register(c echo.Context) error {
var req RegisterRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
// Check if user already exists
if _, err := h.db.GetUserByEmail(c.Request().Context(), req.Email); err == nil {
return c.JSON(http.StatusConflict, map[string]string{"error": "email already exists"})
}
if _, err := h.db.GetUserByUsername(c.Request().Context(), req.Username); err == nil {
return c.JSON(http.StatusConflict, map[string]string{"error": "username already exists"})
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
}
// Create user
user, err := h.db.CreateUser(c.Request().Context(), database.CreateUserParams{
Email: req.Email,
Username: req.Username,
PasswordHash: string(hashedPassword),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
// Generate JWT
token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String())
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
}
return c.JSON(http.StatusCreated, AuthResponse{
Token: token,
User: UserProfile{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Username: user.Username,
},
})
}
// Login handles POST /api/auth/login
func (h *AuthHandler) Login(c echo.Context) error {
var req LoginRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
// Get user by email or username
user, err := h.db.GetUserByEmailOrUsername(c.Request().Context(), req.Login)
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
}
// Check password
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
}
// Generate JWT
token, err := h.generateJWT(uuid.UUID(user.ID.Bytes).String())
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"})
}
return c.JSON(http.StatusOK, AuthResponse{
Token: token,
User: UserProfile{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Username: user.Username,
},
})
}
// GetProfile handles GET /api/auth/profile
func (h *AuthHandler) GetProfile(c echo.Context) error {
userID := c.Get("user_id").(string)
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"})
}
user, err := h.db.GetUser(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
}
return c.JSON(http.StatusOK, UserProfile{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Username: user.Username,
})
}
func (h *AuthHandler) generateJWT(userID string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(h.jwtKey)
}
+252
View File
@@ -0,0 +1,252 @@
package handlers
import (
"bookmann/internal/database"
"net/http"
"strconv"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
)
type Handler struct {
db *database.Queries
}
func NewHandler(db *database.Queries) *Handler {
return &Handler{
db: db,
}
}
func SetupRoutes(g *echo.Group, db *database.Queries) {
h := NewHandler(db)
g.GET("/ebooks", h.ListEbooks)
g.GET("/ebooks/:id", h.GetEbook)
g.POST("/ebooks", h.CreateEbook)
g.PUT("/ebooks/:id", h.UpdateEbook)
g.DELETE("/ebooks/:id", h.DeleteEbook)
g.GET("/ebooks/:id/progress", h.GetReadingProgress)
g.PUT("/ebooks/:id/progress", h.UpdateReadingProgress)
}
// ListEbooks handles GET /api/ebooks
func (h *Handler) ListEbooks(c echo.Context) error {
limitStr := c.QueryParam("limit")
offsetStr := c.QueryParam("offset")
limit := int32(20) // default
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil {
limit = int32(l)
}
}
offset := int32(0)
if offsetStr != "" {
if o, err := strconv.Atoi(offsetStr); err == nil {
offset = int32(o)
}
}
ebooks, err := h.db.ListEbooks(c.Request().Context(), database.ListEbooksParams{
Limit: limit,
Offset: offset,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, ebooks)
}
// GetEbook handles GET /api/ebooks/:id
func (h *Handler) GetEbook(c echo.Context) error {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
}
ebook, err := h.db.GetEbook(c.Request().Context(), pgtype.UUID{Bytes: id, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, ebook)
}
// CreateEbookRequest represents the request for creating an ebook
type CreateEbookRequest struct {
Title string `json:"title" validate:"required,min=1,max=500"`
Author string `json:"author"`
ISBN string `json:"isbn"`
Description string `json:"description"`
FilePath string `json:"file_path" validate:"required"`
FileSize int64 `json:"file_size" validate:"required,min=1"`
MimeType string `json:"mime_type" validate:"required"`
CoverImagePath string `json:"cover_image_path"`
}
// CreateEbook handles POST /api/ebooks
func (h *Handler) CreateEbook(c echo.Context) error {
var req CreateEbookRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
ebook, err := h.db.CreateEbook(c.Request().Context(), database.CreateEbookParams{
Title: req.Title,
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
FilePath: req.FilePath,
FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
MimeType: pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""},
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, ebook)
}
// UpdateEbookRequest represents the request for updating an ebook
type UpdateEbookRequest struct {
Title string `json:"title" validate:"required,min=1,max=500"`
Author string `json:"author"`
ISBN string `json:"isbn"`
Description string `json:"description"`
CoverImagePath string `json:"cover_image_path"`
}
// UpdateEbook handles PUT /api/ebooks/:id
func (h *Handler) UpdateEbook(c echo.Context) error {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
}
var req UpdateEbookRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
ebook, err := h.db.UpdateEbook(c.Request().Context(), database.UpdateEbookParams{
ID: pgtype.UUID{Bytes: id, Valid: true},
Title: req.Title,
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, ebook)
}
// DeleteEbook handles DELETE /api/ebooks/:id
func (h *Handler) DeleteEbook(c echo.Context) error {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
}
err = h.db.DeleteEbook(c.Request().Context(), pgtype.UUID{Bytes: id, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.NoContent(http.StatusNoContent)
}
// GetReadingProgress handles GET /api/ebooks/:id/progress
func (h *Handler) GetReadingProgress(c echo.Context) error {
ebookIdStr := c.Param("id")
userID := c.Get("user_id").(string)
ebookId, err := uuid.Parse(ebookIdStr)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
}
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
progress, err := h.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
// If no progress found, return default
return c.JSON(http.StatusOK, map[string]interface{}{
"ebook_id": ebookIdStr,
"user_id": userID,
"current_page": 0,
"total_pages": nil,
})
}
return c.JSON(http.StatusOK, progress)
}
// UpdateReadingProgressRequest represents the request for updating reading progress
type UpdateReadingProgressRequest struct {
CurrentPage int32 `json:"current_page" validate:"required,min=0"`
TotalPages int32 `json:"total_pages" validate:"omitempty,min=1"`
}
// UpdateReadingProgress handles PUT /api/ebooks/:id/progress
func (h *Handler) UpdateReadingProgress(c echo.Context) error {
ebookIdStr := c.Param("id")
userID := c.Get("user_id").(string)
ebookId, err := uuid.Parse(ebookIdStr)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid ebook id"})
}
userUUID, err := uuid.Parse(userID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
}
var req UpdateReadingProgressRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
progress, err := h.db.UpdateReadingProgress(c.Request().Context(), database.UpdateReadingProgressParams{
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
CurrentPage: pgtype.Int4{Int32: req.CurrentPage, Valid: true},
TotalPages: pgtype.Int4{Int32: req.TotalPages, Valid: req.TotalPages > 0},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, progress)
}
+1
View File
@@ -0,0 +1 @@
01f32890b5356f7cc554a887ad06bc0a298c4dd3
@@ -0,0 +1,43 @@
-- Create users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create ebooks table
CREATE TABLE ebooks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
author VARCHAR(255),
isbn VARCHAR(13),
description TEXT,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
mime_type VARCHAR(100),
cover_image_path VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create reading_progress table
CREATE TABLE reading_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ebook_id UUID NOT NULL REFERENCES ebooks(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
current_page INTEGER DEFAULT 0,
total_pages INTEGER,
last_read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(ebook_id, user_id)
);
-- Create indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_ebooks_title ON ebooks(title);
CREATE INDEX idx_ebooks_author ON ebooks(author);
CREATE INDEX idx_reading_progress_ebook_id ON reading_progress(ebook_id);
CREATE INDEX idx_reading_progress_user_id ON reading_progress(user_id);
+977
View File
@@ -0,0 +1,977 @@
{
"name": "backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "backend",
"version": "1.0.0",
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"name": "backend",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"cors": "^2.8.5",
"body-parser": "^1.20.2"
}
}
+1
View File
@@ -0,0 +1 @@
a6aeff4b597d6c2bdc67497032e46b85148c773e
+107
View File
@@ -0,0 +1,107 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
const PORT = 8080;
const JWT_SECRET = 'your-secret-key'; // In production, use env var
app.use(cors());
app.use(bodyParser.json());
// In-memory user store (for demo purposes)
const users = [];
// Password validation regex
const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
// Middleware to verify token
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Access token required' });
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
}
// Auth routes
app.post('/api/auth/register', async (req, res) => {
const { email, username, password } = req.body;
if (!email || !username || !password) {
return res.status(400).json({ error: 'All fields are required' });
}
// Check if password meets requirements
if (!passwordRegex.test(password)) {
return res.status(400).json({
error: 'Password must be at least 8 characters and include at least one uppercase letter, one number, and one symbol'
});
}
// Check if user already exists
const existingUser = users.find(u => u.email === email || u.username === username);
if (existingUser) {
return res.status(400).json({ error: 'User already exists' });
}
try {
const passwordHash = await bcrypt.hash(password, 10);
const user = { id: users.length + 1, email, username, passwordHash };
users.push(user);
const token = jwt.sign({ id: user.id, email: user.email, username: user.username }, JWT_SECRET);
res.json({ token, user: { id: user.id, email: user.email, username: user.username } });
} catch (error) {
res.status(500).json({ error: 'Registration failed' });
}
});
app.post('/api/auth/login', async (req, res) => {
const { login, password } = req.body; // login can be email or username
if (!login || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}
const user = users.find(u => u.email === login || u.username === login);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
try {
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id, email: user.email, username: user.username }, JWT_SECRET);
res.json({ token, user: { id: user.id, email: user.email, username: user.username } });
} catch (error) {
res.status(500).json({ error: 'Login failed' });
}
});
app.get('/api/auth/profile', authenticateToken, (req, res) => {
res.json(req.user);
});
// Stub routes for ebooks (to avoid errors)
app.get('/api/ebooks', authenticateToken, (req, res) => {
res.json([]);
});
app.get('/api/ebooks/:id', authenticateToken, (req, res) => {
res.json({ id: req.params.id, title: 'Stub' });
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
+17
View File
@@ -0,0 +1,17 @@
version: "2"
sql:
- engine: "postgresql"
schema: "migrations"
queries: "internal/database/queries"
gen:
go:
package: "database"
out: "internal/database"
sql_package: "pgx/v5"
emit_db_tags: true
emit_prepared_queries: true
emit_interface: true
emit_exact_table_names: true
emit_empty_slices: true
emit_exported_queries: true
emit_json_tags: true
+1
View File
@@ -0,0 +1 @@
export const env={}
@@ -0,0 +1 @@
.static{position:static}.container{width:100%}.mx-auto{margin-inline:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.contents{display:contents}.flex{display:flex}.grid{display:grid}.aspect-\[3\/4\]{aspect-ratio:3/4}.h-full{height:100%}.w-full{width:100%}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.overflow-hidden{overflow:hidden}.object-cover{-o-object-fit:cover;object-fit:cover}.text-center{text-align:center}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,ease);transition-duration:var(--tw-duration,0s)}
@@ -0,0 +1 @@
.line-clamp-2.svelte-1uha8ag{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.line-clamp-3.svelte-1uha8ag{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
@@ -0,0 +1 @@
import{b as c,h as _,a as o,E as d,r as b,H as E,s as T,c as p,d as f}from"./DnPHkIdi.js";import{B as y}from"./kSfNJqxT.js";function v(t,i,h=!1){_&&o();var e=new y(t),u=h?d:0;function n(a,r){if(_){const l=b(t)===E;if(a===l){var s=T();p(s),e.anchor=s,f(!1),e.ensure(a,r),f(!0);return}}e.ensure(a,r)}c(()=>{var a=!1;i((r,s=!0)=>{a=!0,n(s,r)}),a||n(!1,null)},u)}export{v as i};
@@ -0,0 +1 @@
import{t as o,q as n,C as c,v as l}from"./DnPHkIdi.js";function u(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(e){n===null&&u(),c&&n.l!==null?a(n).m.push(e):o(()=>{const t=l(e);if(typeof t=="function")return t})}function a(e){var t=e.l;return t.u??={a:[],b:[],m:[]}}export{r as o};
@@ -0,0 +1 @@
import{h as c,N as f,L as p,g as a,e as g}from"./DnPHkIdi.js";const d=Symbol("is custom element"),h=Symbol("is html");function M(s,r,o,i){var t=A(s);c&&(t[r]=s.getAttribute(r),r==="src"||r==="srcset"||r==="href"&&s.nodeName==="LINK")||t[r]!==(t[r]=o)&&(r==="loading"&&(s[p]=o),o==null?s.removeAttribute(r):typeof o!="string"&&N(s).includes(r)?s[r]=o:s.setAttribute(r,o))}function A(s){return s.__attributes??={[d]:s.nodeName.includes("-"),[h]:s.namespaceURI===f}}var e=new Map;function N(s){var r=s.getAttribute("is")||s.nodeName,o=e.get(r);if(o)return o;e.set(r,o=[]);for(var i,t=s,_=Element.prototype;_!==t;){i=g(t);for(var n in i)i[n].set&&o.push(n);t=a(t)}return o}export{M as s};
@@ -0,0 +1 @@
import{aT as he,aU as pt,aQ as U,y as T,a2 as I,aR as ee,aV as gt}from"./DnPHkIdi.js";import{o as qe}from"./BOU_Z_Ye.js";const V=[];function Se(e,t=he){let n=null;const a=new Set;function r(s){if(pt(e,s)&&(e=s,n)){const c=!V.length;for(const l of a)l[1](),V.push(l,e);if(c){for(let l=0;l<V.length;l+=2)V[l][0](V[l+1]);V.length=0}}}function i(s){r(s(e))}function o(s,c=he){const l=[s,c];return a.add(l),a.size===1&&(n=t(r,i)||he),s(e),()=>{a.delete(l),a.size===0&&n&&(n(),n=null)}}return{set:r,update:i,subscribe:o}}class Ee{constructor(t,n){this.status=t,typeof n=="string"?this.body={message:n}:n?this.body=n:this.body={message:`Error: ${t}`}}toString(){return JSON.stringify(this.body)}}class Re{constructor(t,n){this.status=t,this.location=n}}class xe extends Error{constructor(t,n,a){super(a),this.status=t,this.text=n}}new URL("sveltekit-internal://");function _t(e,t){return e==="/"||t==="ignore"?e:t==="never"?e.endsWith("/")?e.slice(0,-1):e:t==="always"&&!e.endsWith("/")?e+"/":e}function mt(e){return e.split("%25").map(decodeURI).join("%25")}function wt(e){for(const t in e)e[t]=decodeURIComponent(e[t]);return e}function pe({href:e}){return e.split("#")[0]}function vt(...e){let t=5381;for(const n of e)if(typeof n=="string"){let a=n.length;for(;a;)t=t*33^n.charCodeAt(--a)}else if(ArrayBuffer.isView(n)){const a=new Uint8Array(n.buffer,n.byteOffset,n.byteLength);let r=a.length;for(;r;)t=t*33^a[--r]}else throw new TypeError("value must be a string or TypedArray");return(t>>>0).toString(36)}new TextEncoder;new TextDecoder;function yt(e){const t=atob(e),n=new Uint8Array(t.length);for(let a=0;a<t.length;a++)n[a]=t.charCodeAt(a);return n}const bt=window.fetch;window.fetch=(e,t)=>((e instanceof Request?e.method:t?.method||"GET")!=="GET"&&F.delete(Ae(e)),bt(e,t));const F=new Map;function kt(e,t){const n=Ae(e,t),a=document.querySelector(n);if(a?.textContent){a.remove();let{body:r,...i}=JSON.parse(a.textContent);const o=a.getAttribute("data-ttl");return o&&F.set(n,{body:r,init:i,ttl:1e3*Number(o)}),a.getAttribute("data-b64")!==null&&(r=yt(r)),Promise.resolve(new Response(r,i))}return window.fetch(e,t)}function St(e,t,n){if(F.size>0){const a=Ae(e,n),r=F.get(a);if(r){if(performance.now()<r.ttl&&["default","force-cache","only-if-cached",void 0].includes(n?.cache))return new Response(r.body,r.init);F.delete(a)}}return window.fetch(t,n)}function Ae(e,t){let a=`script[data-sveltekit-fetched][data-url=${JSON.stringify(e instanceof Request?e.url:e)}]`;if(t?.headers||t?.body){const r=[];t.headers&&r.push([...new Headers(t.headers)].join(",")),t.body&&(typeof t.body=="string"||ArrayBuffer.isView(t.body))&&r.push(t.body),a+=`[data-hash="${vt(...r)}"]`}return a}const Et=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;function Rt(e){const t=[];return{pattern:e==="/"?/^\/$/:new RegExp(`^${At(e).map(a=>{const r=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(a);if(r)return t.push({name:r[1],matcher:r[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const i=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(a);if(i)return t.push({name:i[1],matcher:i[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!a)return;const o=a.split(/\[(.+?)\](?!\])/);return"/"+o.map((c,l)=>{if(l%2){if(c.startsWith("x+"))return ge(String.fromCharCode(parseInt(c.slice(2),16)));if(c.startsWith("u+"))return ge(String.fromCharCode(...c.slice(2).split("-").map(m=>parseInt(m,16))));const f=Et.exec(c),[,h,w,u,g]=f;return t.push({name:u,matcher:g,optional:!!h,rest:!!w,chained:w?l===1&&o[0]==="":!1}),w?"([^]*?)":h?"([^/]*)?":"([^/]+?)"}return ge(c)}).join("")}).join("")}/?$`),params:t}}function xt(e){return e!==""&&!/^\([^)]+\)$/.test(e)}function At(e){return e.slice(1).split("/").filter(xt)}function Lt(e,t,n){const a={},r=e.slice(1),i=r.filter(s=>s!==void 0);let o=0;for(let s=0;s<t.length;s+=1){const c=t[s];let l=r[s-o];if(c.chained&&c.rest&&o&&(l=r.slice(s-o,s+1).filter(f=>f).join("/"),o=0),l===void 0){c.rest&&(a[c.name]="");continue}if(!c.matcher||n[c.matcher](l)){a[c.name]=l;const f=t[s+1],h=r[s+1];f&&!f.rest&&f.optional&&h&&c.chained&&(o=0),!f&&!h&&Object.keys(a).length===i.length&&(o=0);continue}if(c.optional&&c.chained){o++;continue}return}if(!o)return a}function ge(e){return e.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function Ut({nodes:e,server_loads:t,dictionary:n,matchers:a}){const r=new Set(t);return Object.entries(n).map(([s,[c,l,f]])=>{const{pattern:h,params:w}=Rt(s),u={id:s,exec:g=>{const m=h.exec(g);if(m)return Lt(m,w,a)},errors:[1,...f||[]].map(g=>e[g]),layouts:[0,...l||[]].map(o),leaf:i(c)};return u.errors.length=u.layouts.length=Math.max(u.errors.length,u.layouts.length),u});function i(s){const c=s<0;return c&&(s=~s),[c,e[s]]}function o(s){return s===void 0?s:[r.has(s),e[s]]}}function Ge(e,t=JSON.parse){try{return t(sessionStorage[e])}catch{}}function De(e,t,n=JSON.stringify){const a=n(t);try{sessionStorage[e]=a}catch{}}const x=globalThis.__sveltekit_1nk8k67?.base??"",TLine truncated
@@ -0,0 +1 @@
import{k as d,O as f,a3 as v,a4 as u,a5 as E,a6 as p,h as i,m as o,a7 as h,a as T,a8 as N,c as g}from"./DnPHkIdi.js";function y(r){var n=document.createElement("template");return n.innerHTML=r.replaceAll("<!>","<!---->"),n.content}function a(r,n){var e=u;e.nodes===null&&(e.nodes={start:r,end:n,a:null,t:null})}function A(r,n){var e=(n&E)!==0,l=(n&p)!==0,t,_=!r.startsWith("<!>");return()=>{if(i)return a(o,null),o;t===void 0&&(t=y(_?r:"<!>"+r),e||(t=f(t)));var s=l||v?document.importNode(t,!0):t.cloneNode(!0);if(e){var c=f(s),m=s.lastChild;a(c,m)}else a(s,s);return s}}function M(r=""){if(!i){var n=d(r+"");return a(n,n),n}var e=o;return e.nodeType!==N&&(e.before(e=d()),g(e)),a(e,e),e}function O(){if(i)return a(o,null),o;var r=document.createDocumentFragment(),n=document.createComment(""),e=d();return r.append(n,e),a(n,e),r}function C(r,n){if(i){var e=u;((e.f&h)===0||e.nodes.end===null)&&(e.nodes.end=o),T();return}r!==null&&r.before(n)}const w="5";typeof window<"u"&&((window.__svelte??={}).v??=new Set).add(w);export{C as a,a as b,O as c,A as f,M as t};
@@ -0,0 +1,2 @@
import{aa as U,y as P,V as j,ab as G,v as J,ac as B,ad as W,m as d,h as c,a4 as b,b as K,a as X,Q as x,H as Z,l as _,ae as T,p as k,k as $,af as ee,ag as S,ah as y,ai as C,aj as te,ak as O,q as z,n as se,al as M,am as ie,an as V,ao as re,S as ne,j as A,c as D,ap as ae,s as he,aq as q,ar as oe,E as fe,as as le,at as ue,au as _e,av as de,aw as F,O as ce,ax as pe,a0 as ge,ay as I,d as w,az as ve,a1 as ye,T as me,aA as be,D as Ee,R as Te,aB as we,I as Re}from"./DnPHkIdi.js";import{b as Se}from"./DUCk1qN8.js";function De(t){let e=0,i=j(0),r;return()=>{U()&&(P(i),G(()=>(e===0&&(r=J(()=>t(()=>B(i)))),e+=1,()=>{W(()=>{e-=1,e===0&&(r?.(),r=void 0,B(i))})})))}}var Ne=fe|le|ue;function ke(t,e,i){new Ae(t,e,i)}class Ae{parent;is_pending=!1;#t;#v=c?d:null;#i;#l;#r;#s=null;#e=null;#n=null;#a=null;#o=null;#u=0;#h=0;#_=!1;#d=new Set;#c=new Set;#f=null;#b=De(()=>(this.#f=j(this.#u),()=>{this.#f=null}));constructor(e,i,r){this.#t=e,this.#i=i,this.#l=r,this.parent=b.b,this.is_pending=!!this.#i.pending,this.#r=K(()=>{if(b.b=this,c){const s=this.#v;X(),s.nodeType===x&&s.data===Z?this.#T():(this.#E(),this.#h===0&&(this.is_pending=!1))}else{var a=this.#y();try{this.#s=_(()=>r(a))}catch(s){this.error(s)}this.#h>0?this.#g():this.is_pending=!1}return()=>{this.#o?.remove()}},Ne),c&&(this.#t=d)}#E(){try{this.#s=_(()=>this.#l(this.#t))}catch(e){this.error(e)}}#T(){const e=this.#i.pending;e&&(this.#e=_(()=>e(this.#t)),T.enqueue(()=>{var i=this.#y();this.#s=this.#p(()=>(T.ensure(),_(()=>this.#l(i)))),this.#h>0?this.#g():(k(this.#e,()=>{this.#e=null}),this.is_pending=!1)}))}#y(){var e=this.#t;return this.is_pending&&(this.#o=$(),this.#t.before(this.#o),e=this.#o),e}defer_effect(e){ee(e,this.#d,this.#c)}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!this.#i.pending}#p(e){var i=b,r=O,a=z;S(this.#r),y(this.#r),C(this.#r.ctx);try{return e()}catch(s){return te(s),null}finally{S(i),y(r),C(a)}}#g(){const e=this.#i.pending;this.#s!==null&&(this.#a=document.createDocumentFragment(),this.#a.append(this.#o),se(this.#s,this.#a)),this.#e===null&&(this.#e=_(()=>e(this.#t)))}#m(e){if(!this.has_pending_snippet()){this.parent&&this.parent.#m(e);return}if(this.#h+=e,this.#h===0){this.is_pending=!1;for(const i of this.#d)M(i,ie),V(i);for(const i of this.#c)M(i,re),V(i);this.#d.clear(),this.#c.clear(),this.#e&&k(this.#e,()=>{this.#e=null}),this.#a&&(this.#t.before(this.#a),this.#a=null)}}update_pending_count(e){this.#m(e),this.#u+=e,this.#f&&ne(this.#f,this.#u)}get_effect_pending(){return this.#b(),P(this.#f)}error(e){var i=this.#i.onerror;let r=this.#i.failed;if(this.#_||!i&&!r)throw e;this.#s&&(A(this.#s),this.#s=null),this.#e&&(A(this.#e),this.#e=null),this.#n&&(A(this.#n),this.#n=null),c&&(D(this.#v),ae(),D(he()));var a=!1,s=!1;const o=()=>{if(a){_e();return}a=!0,s&&oe(),T.ensure(),this.#u=0,this.#n!==null&&k(this.#n,()=>{this.#n=null}),this.is_pending=this.has_pending_snippet(),this.#s=this.#p(()=>(this.#_=!1,_(()=>this.#l(this.#t)))),this.#h>0?this.#g():this.is_pending=!1};var l=O;try{y(null),s=!0,i?.(e,o),s=!1}catch(f){q(f,this.#r&&this.#r.parent)}finally{y(l)}r&&W(()=>{this.#n=this.#p(()=>{T.ensure(),this.#_=!0;try{return _(()=>{r(this.#t,()=>e,()=>o)})}catch(f){return q(f,this.#r.parent),null}finally{this.#_=!1}})})}}const Oe=["touchstart","touchmove"];function Fe(t){return Oe.includes(t)}const Ie=new Set,H=new Set;let L=null;function R(t){var e=this,i=e.ownerDocument,r=t.type,a=t.composedPath?.()||[],s=a[0]||t.target;L=t;var o=0,l=L===t&&t.__root;if(l){var f=a.indexOf(l);if(f!==-1&&(e===document||e===window)){t.__root=e;return}var p=a.indexOf(e);if(p===-1)return;f<=p&&(o=f)}if(s=a[o]||t.target,s!==e){de(t,"currentTarget",{configurable:!0,get(){return s||i}});var N=O,u=b;y(null),S(null);try{for(var n,h=[];s!==null;){var g=s.assignedSlot||s.parentNode||s.host||null;try{var m=s["__"+r];m!=null&&(!s.disabled||t.target===s)&&m.call(s,t)}catch(E){n?h.push(E):n=E}if(t.cancelBubble||g===e||g===null)break;s=g}if(n){for(let E of h)queueMicrotask(()=>{throw E});throw n}}finally{t.__root=e,delete t.currentTarget,y(N),S(u)}}}function Ce(t,e){var i=e==null?"":typeof e=="object"?e+"":e;i!==(t.__t??=t.nodeValue)&&(t.__t=i,t.nodeValue=i+"")}function Ye(t,e){return Q(t,e)}function Me(t,e){F(),e.intro=e.intro??!1;const i=e.target,r=c,a=d;try{for(var s=ce(i);s&&(s.nodeType!==x||s.data!==pe);)s=ge(s);if(!s)throw I;w(!0),D(s);const o=Q(t,{...e,anchor:s});return w(!1),o}catch(o){if(o instanceof Error&&o.message.split(`
`).some(l=>l.startsWith("https://svelte.dev/e/")))throw o;return o!==I&&console.warn("Failed to hydrate: ",o),e.recover===!1&&ve(),F(),ye(i),w(!1),Ye(t,e)}finally{w(r),D(a)}}const v=new Map;function Q(t,{target:e,anchor:i,props:r={},events:a,context:s,intro:o=!0}){F();var l=new Set,f=u=>{for(var n=0;n<u.length;n++){var h=u[n];if(!l.has(h)){l.add(h);var g=Fe(h);e.addEventListener(h,R,{passive:g});var m=v.get(h);m===void 0?(document.addEventListener(h,R,{passive:g}),v.set(h,1)):v.set(h,m+1)}}};f(me(Ie)),H.add(f);var p=void 0,N=be(()=>{var u=i??e.appendChild($());return ke(u,{pending:()=>{}},n=>{if(s){Ee({});var h=z;h.c=s}if(a&&(r.$$events=a),c&&Se(n,null),p=t(n,r)||{},c&&(b.nodes.end=d,d===null||d.nodeType!==x||d.data!==Te))throw we(),I;s&&Re()}),()=>{for(var n of l){e.removeEventListener(n,R);var h=v.get(n);--h===0?(document.removeEventListener(n,R),v.delete(n)):v.set(n,h)}H.delete(f),u!==i&&u.parentNode?.removeChild(u)}});return Y.set(p,N),p}let Y=new WeakMap;function Ve(t,e){const i=Y.get(t);return i?(Y.delete(t),i(e)):Promise.resolve()}export{Me as h,Ye as m,Ce as s,Ve as u};
@@ -0,0 +1 @@
var Ft=Array.isArray,Mt=Array.prototype.indexOf,bn=Array.from,Tn=Object.defineProperty,ae=Object.getOwnPropertyDescriptor,jt=Object.getOwnPropertyDescriptors,Lt=Object.prototype,qt=Array.prototype,We=Object.getPrototypeOf,ze=Object.isExtensible;const An=()=>{};function Rn(e){return e()}function Yt(e){for(var t=0;t<e.length;t++)e[t]()}function et(){var e,t,n=new Promise((r,s)=>{e=r,t=s});return{promise:n,resolve:e,reject:t}}const w=2,Ee=4,_e=8,tt=1<<24,M=16,j=32,te=64,nt=128,k=512,E=1024,R=2048,B=4096,P=8192,q=16384,je=32768,ge=65536,$e=1<<17,rt=1<<18,ve=1<<19,st=1<<20,Sn=1<<25,X=32768,Ne=1<<21,Le=1<<22,Y=1<<23,le=Symbol("$state"),xn=Symbol("legacy props"),On=Symbol(""),ne=new class extends Error{name="StaleReactionError";message="The reaction that called `getAbortSignal()` was re-run or destroyed"},qe=3,ft=8;function Ut(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function Ht(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function Bt(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function Vt(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Gt(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Dn(){throw new Error("https://svelte.dev/e/hydration_failed")}function Nn(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function Kt(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function zt(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function $t(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function Pn(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const In=1,Cn=2,Fn=16,Mn=1,jn=2,Ln=4,qn=8,Yn=16,Un=1,Hn=2,Xt="[",Zt="[!",Jt="]",Ye={},y=Symbol(),Bn="http://www.w3.org/1999/xhtml";function Ue(e){console.warn("https://svelte.dev/e/hydration_mismatch")}function Vn(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}let Z=!1;function Gn(e){Z=e}let A;function re(e){if(e===null)throw Ue(),Ye;return A=e}function Kn(){return re(V(A))}function zn(e){if(Z){if(V(A)!==null)throw Ue(),Ye;A=e}}function $n(e=1){if(Z){for(var t=e,n=A;t--;)n=V(n);A=n}}function Xn(e=!0){for(var t=0,n=A;;){if(n.nodeType===ft){var r=n.data;if(r===Jt){if(t===0)return n;t-=1}else(r===Xt||r===Zt)&&(t+=1)}var s=V(n);e&&n.remove(),n=s}}function Zn(e){if(!e||e.nodeType!==ft)throw Ue(),Ye;return e.data}function it(e){return e===this.v}function Qt(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function at(e){return!Qt(e,this.v)}let xe=!1;function Jn(){xe=!0}let b=null;function me(e){b=e}function Qn(e,t=!1,n){b={p:b,i:!1,c:null,e:null,s:e,x:null,l:xe&&!t?{s:null,u:null,$:[]}:null}}function Wn(e){var t=b,n=t.e;if(n!==null){t.e=null;for(var r of n)bt(r)}return t.i=!0,b=t.p,{}}function de(){return!xe||b!==null&&b.l===null}let K=[];function lt(){var e=K;K=[],Yt(e)}function Wt(e){if(K.length===0&&!ue){var t=K;queueMicrotask(()=>{t===K&&lt()})}K.push(e)}function en(){for(;K.length>0;)lt()}function tn(e){var t=h;if(t===null)return _.f|=Y,e;if((t.f&je)===0){if((t.f&nt)===0)throw e;t.b.error(e)}else be(e,t)}function be(e,t){for(;t!==null;){if((t.f&nt)!==0)try{t.b.error(e);return}catch(n){e=n}t=t.parent}throw e}const nn=-7169;function g(e,t){e.f=e.f&nn|t}function He(e){(e.f&k)!==0||e.deps===null?g(e,E):g(e,B)}function ut(e){if(e!==null)for(const t of e)(t.f&w)===0||(t.f&X)===0||(t.f^=X,ut(t.deps))}function rn(e,t,n){(e.f&R)!==0?t.add(e):(e.f&B)!==0&&n.add(e),ut(e.deps),g(e,E)}const we=new Set;let p=null,D=null,O=[],Oe=null,Pe=!1,ue=!1;class J{committed=!1;current=new Map;previous=new Map;#n=new Set;#r=new Set;#e=0;#t=0;#i=null;#s=new Set;#f=new Set;skipped_effects=new Set;is_fork=!1;is_deferred(){return this.is_fork||this.#t>0}process(t){O=[],this.apply();var n=[],r=[];for(const s of t)this.#a(s,n,r);this.is_fork||this.#u(),this.is_deferred()?(this.#l(r),this.#l(n)):(p=null,Xe(r),Xe(n),this.#i?.resolve()),D=null}#a(t,n,r){t.f^=E;for(var s=t.first,f=null;s!==null;){var i=s.f,l=(i&(j|te))!==0,a=l&&(i&E)!==0,u=a||(i&P)!==0||this.skipped_effects.has(s);if(!u&&s.fn!==null){l?s.f^=E:f!==null&&(i&(Ee|_e|tt))!==0?f.b.defer_effect(s):(i&Ee)!==0?n.push(s):he(s)&&((i&M)!==0&&this.#s.add(s),ce(s));var o=s.first;if(o!==null){s=o;continue}}var c=s.parent;for(s=s.next;s===null&&c!==null;)c===f&&(f=null),s=c.next,c=c.parent}}#l(t){for(var n=0;n<t.length;n+=1)rn(t[n],this.#s,this.#f)}capture(t,n){n!==y&&!this.previous.has(t)&&this.previous.set(t,n),(t.f&Y)===0&&(this.current.set(t,t.v),D?.set(t,t.v))}activate(){p=this,this.apply()}deactivate(){p===this&&(p=null,D=null)}flush(){if(this.activate(),O.length>0){if(ot(),p!==null&&p!==this)return}else this.#e===0&&this.process([]);this.deactivate()}discard(){for(const t of this.#r)t(this);this.#r.clear()}#u(){if(this.#t===0){for(const t of this.#n)t();this.#n.clear()}this.#e===0&&this.#o()}#o(){if(we.size>1){this.previous.clear();var t=D,n=!0;for(const s of we){if(s===this){n=!1;continue}const f=[];for(conLine truncated
@@ -0,0 +1 @@
import{q as d,u as g,t as c,v as m,w as i,x as b,y as p,z as v,A as y,B as h}from"./DnPHkIdi.js";function x(n=!1){const s=d,e=s.l.u;if(!e)return;let f=()=>v(s.s);if(n){let a=0,t={};const _=y(()=>{let l=!1;const r=s.s;for(const o in r)r[o]!==t[o]&&(t[o]=r[o],l=!0);return l&&a++,a});f=()=>p(_)}e.b.length&&g(()=>{u(s,f),i(e.b)}),c(()=>{const a=m(()=>e.m.map(b));return()=>{for(const t of a)typeof t=="function"&&t()}}),e.a.length&&c(()=>{u(s,f),i(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}h();export{x as i};
@@ -0,0 +1 @@
import{f as n,i as p,j as o,p as u,k as d,l,h as m,m as _,n as v,o as b}from"./DnPHkIdi.js";class w{anchor;#t=new Map;#s=new Map;#e=new Map;#i=new Set;#a=!0;constructor(e,s=!0){this.anchor=e,this.#a=s}#f=()=>{var e=n;if(this.#t.has(e)){var s=this.#t.get(e),t=this.#s.get(s);if(t)p(t),this.#i.delete(s);else{var a=this.#e.get(s);a&&(this.#s.set(s,a.effect),this.#e.delete(s),a.fragment.lastChild.remove(),this.anchor.before(a.fragment),t=a.effect)}for(const[i,f]of this.#t){if(this.#t.delete(i),i===e)break;const h=this.#e.get(f);h&&(o(h.effect),this.#e.delete(f))}for(const[i,f]of this.#s){if(i===s||this.#i.has(i))continue;const h=()=>{if(Array.from(this.#t.values()).includes(i)){var c=document.createDocumentFragment();v(f,c),c.append(d()),this.#e.set(i,{effect:f,fragment:c})}else o(f);this.#i.delete(i),this.#s.delete(i)};this.#a||!t?(this.#i.add(i),u(f,h,!1)):h()}}};#h=e=>{this.#t.delete(e);const s=Array.from(this.#t.values());for(const[t,a]of this.#e)s.includes(t)||(o(a.effect),this.#e.delete(t))};ensure(e,s){var t=n,a=b();if(s&&!this.#s.has(e)&&!this.#e.has(e))if(a){var i=document.createDocumentFragment(),f=d();i.append(f),this.#e.set(e,{effect:l(()=>s(f)),fragment:i})}else this.#s.set(e,l(()=>s(this.anchor)));if(this.#t.set(t,e),a){for(const[h,r]of this.#s)h===e?t.skipped_effects.delete(r):t.skipped_effects.add(r);for(const[h,r]of this.#e)h===e?t.skipped_effects.delete(r.effect):t.skipped_effects.add(r.effect);t.oncommit(this.#f),t.ondiscard(this.#h)}else m&&(this.anchor=_),this.#f()}}export{w as B};
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.Cf73ubsW.js","../chunks/DUCk1qN8.js","../chunks/DnPHkIdi.js","../chunks/kSfNJqxT.js","../chunks/C9HAc536.js","../assets/0.DLollcCP.css","../nodes/1.Mwi0d7FS.js","../chunks/VM9GfMNA.js","../chunks/DZQwV0xP.js","../chunks/CKL1QNnB.js","../chunks/BOU_Z_Ye.js","../nodes/2.BQz_jWEl.js","../chunks/6OCXa8_L.js","../assets/2.D5pqjzFo.css"])))=>i.map(i=>d[i]);
import{h as z,a as H,b as Q,E as Z,aC as X,ab as p,v as U,ad as $,aD as q,aE as ee,aF as te,aG as re,y as v,A as ae,P as ne,aH as se,a2 as O,aI as ie,a4 as oe,aJ as ce,aK as ue,C as fe,aL as le,aM as de,aN as _e,aO as F,aP as me,av as ve,W as he,D as ge,u as be,t as ye,aQ as I,aR as Ee,F as w,M as Pe,I as Se,J as Re,K as Oe,aS as L,G as we}from"../chunks/DnPHkIdi.js";import{h as Ae,m as Ie,u as Le,s as Te}from"../chunks/DZQwV0xP.js";import{f as Y,a as R,c as T,t as xe}from"../chunks/DUCk1qN8.js";import{o as Ce}from"../chunks/BOU_Z_Ye.js";import{i as x}from"../chunks/6OCXa8_L.js";import{B as De}from"../chunks/kSfNJqxT.js";function C(t,e,a){z&&H();var c=new De(t);Q(()=>{var s=e()??null;c.ensure(s,s&&(r=>a(r,s)))},Z)}function k(t,e){return t===e||t?.[q]===e}function D(t={},e,a,c){return X(()=>{var s,r;return p(()=>{s=r,r=[],U(()=>{t!==a(...r)&&(e(t,...r),s&&k(a(...s),t)&&e(null,...s))})}),()=>{$(()=>{r&&k(a(...r),t)&&e(null,...r)})}}),t}let A=!1;function Me(t){var e=A;try{return A=!1,[t(),A]}finally{A=e}}function M(t,e,a,c){var s=!fe||(a&le)!==0,r=(a&ue)!==0,n=(a&_e)!==0,i=c,y=!0,P=()=>(y&&(y=!1,i=n?U(c):c),i),u;if(r){var h=q in t||F in t;u=ee(t,e)?.set??(h&&e in t?o=>t[e]=o:void 0)}var d,_=!1;r?[d,_]=Me(()=>t[e]):d=t[e],d===void 0&&c!==void 0&&(d=P(),u&&(s&&te(),u(d)));var l;if(s?l=()=>{var o=t[e];return o===void 0?P():(y=!0,o)}:l=()=>{var o=t[e];return o!==void 0&&(i=void 0),o===void 0?i:o},s&&(a&re)===0)return l;if(u){var f=t.$$legacy;return(function(o,b){return arguments.length>0?((!s||!b||f||_)&&u(b?l():o),o):l()})}var g=!1,m=((a&de)!==0?ae:ne)(()=>(g=!1,l()));r&&v(m);var S=oe;return(function(o,b){if(arguments.length>0){const E=b?v(m):s&&r?se(o):o;return O(m,E),g=!0,i!==void 0&&(i=E),o}return ie&&g||(S.f&ce)!==0?m.v:v(m)})}function Ne(t){return class extends je{constructor(e){super({component:t,...e})}}}class je{#t;#e;constructor(e){var a=new Map,c=(r,n)=>{var i=he(n,!1,!1);return a.set(r,i),i};const s=new Proxy({...e.props||{},$$events:{}},{get(r,n){return v(a.get(n)??c(n,Reflect.get(r,n)))},has(r,n){return n===F?!0:(v(a.get(n)??c(n,Reflect.get(r,n))),Reflect.has(r,n))},set(r,n,i){return O(a.get(n)??c(n,i),i),Reflect.set(r,n,i)}});this.#e=(e.hydrate?Ae:Ie)(e.component,{target:e.target,anchor:e.anchor,props:s,context:e.context,intro:e.intro??!1,recover:e.recover}),(!e?.props?.$$host||e.sync===!1)&&me(),this.#t=s.$$events;for(const r of Object.keys(this.#e))r==="$set"||r==="$destroy"||r==="$on"||ve(this,r,{get(){return this.#e[r]},set(n){this.#e[r]=n},enumerable:!0});this.#e.$set=r=>{Object.assign(s,r)},this.#e.$destroy=()=>{Le(this.#e)}}$set(e){this.#e.$set(e)}$on(e,a){this.#t[e]=this.#t[e]||[];const c=(...s)=>a.call(this,...s);return this.#t[e].push(c),()=>{this.#t[e]=this.#t[e].filter(s=>s!==c)}}$destroy(){this.#e.$destroy()}}const ke="modulepreload",Be=function(t,e){return new URL(t,e).href},B={},N=function(e,a,c){let s=Promise.resolve();if(a&&a.length>0){let P=function(u){return Promise.all(u.map(h=>Promise.resolve(h).then(d=>({status:"fulfilled",value:d}),d=>({status:"rejected",reason:d}))))};const n=document.getElementsByTagName("link"),i=document.querySelector("meta[property=csp-nonce]"),y=i?.nonce||i?.getAttribute("nonce");s=P(a.map(u=>{if(u=Be(u,c),u in B)return;B[u]=!0;const h=u.endsWith(".css"),d=h?'[rel="stylesheet"]':"";if(c)for(let l=n.length-1;l>=0;l--){const f=n[l];if(f.href===u&&(!h||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${u}"]${d}`))return;const _=document.createElement("link");if(_.rel=h?"stylesheet":ke,h||(_.as="script"),_.crossOrigin="",_.href=u,y&&_.setAttribute("nonce",y),document.head.appendChild(_),h)return new Promise((l,f)=>{_.addEventListener("load",l),_.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${u}`)))})}))}function r(n){const i=new Event("vite:preloadError",{cancelable:!0});if(i.payload=n,window.dispatchEvent(i),!i.defaultPrevented)throw n}return s.then(n=>{for(const i of n||[])i.status==="rejected"&&r(i.reason);return e().catch(r)})},Qe={};var Ue=Y('<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"><!></div>'),qe=Y("<!> <!>",1);function Fe(t,e){ge(e,!0);let a=M(e,"components",23,()=>[]),c=M(e,"data_0",3,null),s=M(e,"data_1",3,null);be(()=>e.stores.page.set(e.page)),ye(()=>{e.stores,e.page,e.constructors,a(),e.form,c(),s(),e.stores.page.notify()});let r=I(!1),n=I(!1),i=I(null);Ce(()=>{const f=e.stores.page.subscribe(()=>{v(r)&&(O(n,!0),Ee().then(()=>{O(i,document.title||"untitled page",!0)}))});return O(r,!0),f});const y=L(()=>e.constructors[1]);var P=qe(),u=w(P);{var h=f=>{const g=L(()=>e.constructors[0]);var m=T(),S=w(m);C(S,()=>v(g),(o,b)=>{D(b(o,{get data(){return c()},get form(){return e.form},get params(){return e.page.params},children:(E,Ge)=>{var j=T(),V=w(j);C(V,()=>v(y),(J,K)=>{D(K(J,{get data(){return s()},get form(){return e.form},get pLine truncated
@@ -0,0 +1 @@
import{l as o,a as r}from"../chunks/CKL1QNnB.js";export{o as load_css,r as start};
@@ -0,0 +1 @@
import{c as p,a as r,f as v}from"../chunks/DUCk1qN8.js";import{b as m,E as g,k as h,a9 as u,h as n,O as _,Q as y,a0 as o,d as f,c,m as w,F as E,G as x}from"../chunks/DnPHkIdi.js";import{B as T}from"../chunks/kSfNJqxT.js";import{s as b}from"../chunks/C9HAc536.js";function A(l,s,...a){var i=new T(l);m(()=>{const t=s()??null;i.ensure(t,t&&(e=>t(e,...a)))},g)}function F(l,s){let a=null,i=n;var t;if(n){a=w;for(var e=_(document.head);e!==null&&(e.nodeType!==y||e.data!==l);)e=o(e);if(e===null)f(!1);else{var d=o(e);e.remove(),c(d)}}n||(t=document.head.appendChild(h()));try{m(()=>s(t),u)}finally{i&&(f(!0),c(a))}}const M="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='107'%20height='128'%20viewBox='0%200%20107%20128'%3e%3ctitle%3esvelte-logo%3c/title%3e%3cpath%20d='M94.157%2022.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282%2029.608A29.92%2029.92%200%200%200%208.764%2049.65a31.5%2031.5%200%200%200%203.108%2020.231%2030%2030%200%200%200-4.477%2011.183%2031.9%2031.9%200%200%200%205.448%2024.116c10.402%2014.887%2030.942%2019.297%2045.791%209.835l26.083-16.624A29.92%2029.92%200%200%200%2098.235%2078.35a31.53%2031.53%200%200%200-3.105-20.232%2030%2030%200%200%200%204.474-11.182%2031.88%2031.88%200%200%200-5.447-24.116'%20style='fill:%23ff3e00'/%3e%3cpath%20d='M45.817%20106.582a20.72%2020.72%200%200%201-22.237-8.243%2019.17%2019.17%200%200%201-3.277-14.503%2018%2018%200%200%201%20.624-2.435l.49-1.498%201.337.981a33.6%2033.6%200%200%200%2010.203%205.098l.97.294-.09.968a5.85%205.85%200%200%200%201.052%203.878%206.24%206.24%200%200%200%206.695%202.485%205.8%205.8%200%200%200%201.603-.704L69.27%2076.28a5.43%205.43%200%200%200%202.45-3.631%205.8%205.8%200%200%200-.987-4.371%206.24%206.24%200%200%200-6.698-2.487%205.7%205.7%200%200%200-1.6.704l-9.953%206.345a19%2019%200%200%201-5.296%202.326%2020.72%2020.72%200%200%201-22.237-8.243%2019.17%2019.17%200%200%201-3.277-14.502%2017.99%2017.99%200%200%201%208.13-12.052l26.081-16.623a19%2019%200%200%201%205.3-2.329%2020.72%2020.72%200%200%201%2022.237%208.243%2019.17%2019.17%200%200%201%203.277%2014.503%2018%2018%200%200%201-.624%202.435l-.49%201.498-1.337-.98a33.6%2033.6%200%200%200-10.203-5.1l-.97-.294.09-.968a5.86%205.86%200%200%200-1.052-3.878%206.24%206.24%200%200%200-6.696-2.485%205.8%205.8%200%200%200-1.602.704L37.73%2051.72a5.42%205.42%200%200%200-2.449%203.63%205.79%205.79%200%200%200%20.986%204.372%206.24%206.24%200%200%200%206.698%202.486%205.8%205.8%200%200%200%201.602-.704l9.952-6.342a19%2019%200%200%201%205.295-2.328%2020.72%2020.72%200%200%201%2022.237%208.242%2019.17%2019.17%200%200%201%203.277%2014.503%2018%2018%200%200%201-8.13%2012.053l-26.081%2016.622a19%2019%200%200%201-5.3%202.328'%20style='fill:%23fff'/%3e%3c/svg%3e";var k=v('<link rel="icon"/>');function O(l,s){var a=p();F("12qhfyh",t=>{var e=k();x(()=>b(e,"href",M)),r(t,e)});var i=E(a);A(i,()=>s.children),r(l,a)}export{O as component};
@@ -0,0 +1 @@
import{f as u,a as h}from"../chunks/DUCk1qN8.js";import{i as g}from"../chunks/VM9GfMNA.js";import{D as l,F as v,G as d,I as _,J as a,K as e,M as x}from"../chunks/DnPHkIdi.js";import{s as o}from"../chunks/DZQwV0xP.js";import{s as $,p}from"../chunks/CKL1QNnB.js";const k={get error(){return p.error},get status(){return p.status}};$.updated.check;const m=k;var b=u("<h1> </h1> <p> </p>",1);function J(i,f){l(f,!1),g();var t=b(),r=v(t),n=a(r,!0);e(r);var s=x(r,2),c=a(s,!0);e(s),d(()=>{o(n,m.status),o(c,m.error?.message)}),h(i,t),_()}export{J as component};
@@ -0,0 +1 @@
import{f as N,a as T,c as te}from"../chunks/DUCk1qN8.js";import{i as _e}from"../chunks/VM9GfMNA.js";import{o as he}from"../chunks/BOU_Z_Ye.js";import{c as J,h as H,O as me,k as K,a as xe,b as Ee,y as _,P as be,r as ye,H as we,s as se,d as Q,m as V,Q as Te,R as Ae,S as ie,f as Ie,l as $,o as Ne,T as ee,U as ke,V as ne,W as P,X as Ce,Y as Se,Z as Me,_ as F,i as ve,p as de,$ as W,a0 as Re,a1 as Fe,j as De,D as He,I as Oe,a2 as Z,M as Y,J as M,K as S,F as le,G as z}from"../chunks/DnPHkIdi.js";import{s as B}from"../chunks/DZQwV0xP.js";import{i as O}from"../chunks/6OCXa8_L.js";import{s as oe}from"../chunks/C9HAc536.js";function ze(e,i){return i}function Le(e,i,s){for(var u=[],h=i.length,n,l=i.length,t=0;t<h;t++){let f=i[t];de(f,()=>{if(n){if(n.pending.delete(f),n.done.add(f),n.pending.size===0){var m=e.outrogroups;j(ee(n.done)),m.delete(n),m.size===0&&(e.outrogroups=null)}}else l-=1},!1)}if(l===0){var v=u.length===0&&s!==null;if(v){var a=s,o=a.parentNode;Fe(o),o.append(a),e.items.clear()}j(i,!v)}else n={pending:new Set(i),done:new Set},(e.outrogroups??=new Set).add(n)}function j(e,i=!0){for(var s=0;s<e.length;s++)De(e[s],i)}var fe;function Ve(e,i,s,u,h,n=null){var l=e,t=new Map;{var v=e;l=H?J(me(v)):v.appendChild(K())}H&&xe();var a=null,o=be(()=>{var p=s();return ke(p)?p:p==null?[]:ee(p)}),f,m=!0;function D(){c.fallback=a,Ye(c,f,l,i,u),a!==null&&(f.length===0?(a.f&F)===0?ve(a):(a.f^=F,L(a,null,l)):de(a,()=>{a=null}))}var r=Ee(()=>{f=_(o);var p=f.length;let I=!1;if(H){var y=ye(l)===we;y!==(p===0)&&(l=se(),J(l),Q(!1),I=!0)}for(var w=new Set,x=Ie,k=Ne(),d=0;d<p;d+=1){H&&V.nodeType===Te&&V.data===Ae&&(l=V,I=!0,Q(!1));var C=f[d],g=u(C,d),A=m?null:t.get(g);A?(A.v&&ie(A.v,C),A.i&&ie(A.i,d),k&&x.skipped_effects.delete(A.e)):(A=Be(t,m?l:fe??=K(),C,g,d,h,i,s),m||(A.e.f|=F),t.set(g,A)),w.add(g)}if(p===0&&n&&!a&&(m?a=$(()=>n(l)):(a=$(()=>n(fe??=K())),a.f|=F)),H&&p>0&&J(se()),!m)if(k){for(const[U,X]of t)w.has(U)||x.skipped_effects.add(X.e);x.oncommit(D),x.ondiscard(()=>{})}else D();I&&Q(!0),_(o)}),c={effect:r,items:t,outrogroups:null,fallback:a};m=!1,H&&(l=V)}function Ye(e,i,s,u,h){var n=i.length,l=e.items,t=e.effect.first,v,a=null,o=[],f=[],m,D,r,c;for(c=0;c<n;c+=1){if(m=i[c],D=h(m,c),r=l.get(D).e,e.outrogroups!==null)for(const g of e.outrogroups)g.pending.delete(r),g.done.delete(r);if((r.f&F)!==0)if(r.f^=F,r===t)L(r,null,s);else{var p=a?a.next:t;r===e.effect.last&&(e.effect.last=r.prev),r.prev&&(r.prev.next=r.next),r.next&&(r.next.prev=r.prev),R(e,a,r),R(e,r,p),L(r,p,s),a=r,o=[],f=[],t=a.next;continue}if((r.f&W)!==0&&ve(r),r!==t){if(v!==void 0&&v.has(r)){if(o.length<f.length){var I=f[0],y;a=I.prev;var w=o[0],x=o[o.length-1];for(y=0;y<o.length;y+=1)L(o[y],I,s);for(y=0;y<f.length;y+=1)v.delete(f[y]);R(e,w.prev,x.next),R(e,a,w),R(e,x,I),t=I,a=x,c-=1,o=[],f=[]}else v.delete(r),L(r,t,s),R(e,r.prev,r.next),R(e,r,a===null?e.effect.first:a.next),R(e,a,r),a=r;continue}for(o=[],f=[];t!==null&&t!==r;)(v??=new Set).add(t),f.push(t),t=t.next;if(t===null)continue}(r.f&F)===0&&o.push(r),a=r,t=r.next}if(e.outrogroups!==null){for(const g of e.outrogroups)g.pending.size===0&&(j(ee(g.done)),e.outrogroups?.delete(g));e.outrogroups.size===0&&(e.outrogroups=null)}if(t!==null||v!==void 0){var k=[];if(v!==void 0)for(r of v)(r.f&W)===0&&k.push(r);for(;t!==null;)(t.f&W)===0&&t!==e.fallback&&k.push(t),t=t.next;var d=k.length;if(d>0){var C=n===0?s:null;Le(e,k,C)}}}function Be(e,i,s,u,h,n,l,t){var v=(l&Se)!==0?(l&Me)===0?P(s,!1,!1):ne(s):null,a=(l&Ce)!==0?ne(h):null;return{v,i:a,e:$(()=>(n(i,v??s,a??h,t),()=>{e.delete(u)}))}}function L(e,i,s){if(e.nodes)for(var u=e.nodes.start,h=e.nodes.end,n=i&&(i.f&F)===0?i.nodes.start:s;u!==null;){var l=Re(u);if(n.before(u),u===h)return;u=l}}function R(e,i,s){i===null?e.effect.first=s:i.next=s,s===null?e.effect.last=i:s.prev=i}const Pe="/api";async function Ue(e=20,i=0){const s=await fetch(`${Pe}/ebooks?limit=${e}&offset=${i}`);if(!s.ok)throw new Error("Failed to fetch ebooks");return s.json()}var Xe=N('<div class="text-center py-8">Loading ebooks...</div>'),qe=N('<div class="text-red-500 text-center py-8"> </div>'),Ge=N('<div class="text-center py-8 text-gray-500">No ebooks found. Add some ebooks to get started.</div>'),Je=N('<img class="w-full h-full object-cover"/>'),Ke=N('<div class="text-gray-400 text-center p-4"><div class="text-2xl mb-2">📖</div> <div class="text-sm">No Cover</div></div>'),Qe=N('<p class="text-gray-600 text-sm mb-2"> </p>'),We=N('<p class="text-gray-700 text-sm line-clamp-3 svelte-1uha8ag"> </p>'),Ze=N('<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"><div class="aspect-[3/4] bg-gray-200 flex items-center justify-center"><!></div> <div class="p-4"><h3 class="font-semibold text-lg mb-1 line-clamp-2 svelte-1uha8ag"> </h3> <!> <!></div></div>'),$e=N('<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"></div>'),je=N('<main class="container mx-auto px-4 py-8"><h1 class="text-3xl font-bold mb-8">Ebook Library</h1> <!></mainLine truncated
+1
View File
@@ -0,0 +1 @@
{"version":"1768960079047"}
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="modulepreload" href="/_app/immutable/entry/start.BrST8BG9.js">
<link rel="modulepreload" href="/_app/immutable/chunks/CKL1QNnB.js">
<link rel="modulepreload" href="/_app/immutable/chunks/DnPHkIdi.js">
<link rel="modulepreload" href="/_app/immutable/chunks/BOU_Z_Ye.js">
<link rel="modulepreload" href="/_app/immutable/entry/app.o34c63v3.js">
<link rel="modulepreload" href="/_app/immutable/chunks/DZQwV0xP.js">
<link rel="modulepreload" href="/_app/immutable/chunks/DUCk1qN8.js">
<link rel="modulepreload" href="/_app/immutable/chunks/6OCXa8_L.js">
<link rel="modulepreload" href="/_app/immutable/chunks/kSfNJqxT.js">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">
<script>
{
__sveltekit_1nk8k67 = {
base: ""
};
const element = document.currentScript.parentElement;
Promise.all([
import("/_app/immutable/entry/start.BrST8BG9.js"),
import("/_app/immutable/entry/app.o34c63v3.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
}
</script>
</div>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow: