├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bootstrap ├── .env.local ├── .gitignore ├── Makefile ├── app │ ├── assets │ │ ├── app.css │ │ └── index.js │ ├── conf │ │ └── conf.go │ ├── db │ │ ├── db.go │ │ └── migrations │ │ │ ├── 20240610161057_create_users_table.sql │ │ │ └── 20240610163918_add_sessions_table.sql │ ├── events.go │ ├── events │ │ └── auth.go │ ├── handlers │ │ ├── auth.go │ │ └── landing.go │ ├── routes.go │ ├── types │ │ └── auth.go │ └── views │ │ ├── components │ │ ├── navigation.templ │ │ └── theme_switcher.templ │ │ ├── errors │ │ ├── 404.templ │ │ └── 500.templ │ │ ├── landing │ │ └── index.templ │ │ └── layouts │ │ ├── app_layout.templ │ │ └── base_layout.templ ├── cmd │ ├── app │ │ └── main.go │ └── scripts │ │ └── seed │ │ └── main.go ├── go.mod ├── go.sum ├── package-lock.json ├── package.json ├── pkg │ └── .keep ├── plugins │ └── auth │ │ ├── auth_handler.go │ │ ├── email_verification_error_view.templ │ │ ├── login_view.templ │ │ ├── profile_handler.go │ │ ├── profile_view.templ │ │ ├── routes.go │ │ ├── signup_handler.go │ │ ├── signup_view.templ │ │ └── types.go ├── public │ ├── assets │ │ ├── favicon.ico │ │ ├── index.js │ │ └── styles.css │ └── public.go └── tailwind.config.js ├── db └── db.go ├── event ├── event.go └── event_test.go ├── go.mod ├── go.sum ├── install.go ├── kit ├── kit.go └── middleware │ └── middleware.go ├── ui ├── button │ └── button.go ├── card │ └── card.templ ├── input │ └── input.go ├── modal │ └── modal.templ ├── table │ └── table.templ └── ui.go ├── validate ├── README.md ├── rules.go ├── validate.go └── validate_test.go └── view └── view.go /.gitignore: -------------------------------------------------------------------------------- 1 | _bootstrap -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 @anthdm and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @go test -v ./... 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SUPERKIT 2 | 3 | Build high-performance apps swiftly with minimal team resources in Go programming language. **SUPERKIT** is a full-stack web framework that provides a set of tools and libraries to help you build web applications quickly and efficiently. **SUPERKIT** is built on top of the Go programming language and is designed to be simple and easy to use. 4 | 5 | > The project (for now) is in the **experimental** phase. 6 | 7 | ## Table of content 8 | 9 | - [SUPERKIT](#superkit) 10 | - [Table of content](#table-of-content) 11 | - [Installation](#installation) 12 | - [Getting started](#getting-started) 13 | - [Application structure](#application-structure) 14 | - [assets](#assets) 15 | - [conf](#conf) 16 | - [db](#db) 17 | - [events](#events) 18 | - [handlers](#handlers) 19 | - [types](#types) 20 | - [views](#views) 21 | - [Development server](#development-server) 22 | - [Hot reloading the browser](#hot-reloading-the-browser) 23 | - [Migrations](#migrations) 24 | - [Create a new migration](#create-a-new-migration) 25 | - [Migrate the database](#migrate-the-database) 26 | - [Reset the database](#reset-the-database) 27 | - [Seeds](#seeds) 28 | - [Creating views with Templ](#creating-views-with-templ) 29 | - [Validations](#validations) 30 | - [Testing](#testing) 31 | - [Testing handlers](#testing-handlers) 32 | - [Create a production release](#create-a-production-release) 33 | 34 | ## Installation 35 | 36 | To create a new **SUPERKIT** project, you can run the following command: 37 | 38 | ```bash 39 | # Create your SUPERKIT project in a single command: 40 | go run github.com/anthdm/superkit@master [yourprojectname] 41 | 42 | # You can now navigate to your project: 43 | cd [myprojectname] 44 | 45 | # Run npm install to install both tailwindcss and esbuild locally. 46 | npm install 47 | 48 | # If you run into dependency issues you can run: 49 | go clean -modcache && go get -u ./... 50 | 51 | # If you have the authentication plugin enabled you need to migrate your database. 52 | make db-up 53 | ``` 54 | 55 | ## Getting started 56 | 57 | ## Application structure 58 | 59 | The **SUPERKIT** project structure is designed to be simple and easy to understand. The project structure is as follows: 60 | 61 | ```bash 62 | ├── bootstrap 63 | │ ├── app 64 | │ ├── assets 65 | │ ├── conf 66 | │ ├── db 67 | │ ├── migrations 68 | │ ├── events 69 | │ ├── handlers 70 | │ ├── types 71 | │ ├── views 72 | │ ├── components 73 | │ ├── errors 74 | │ ├── landing 75 | │ ├── layouts 76 | │ ├── cmd 77 | │ ├── app 78 | │ ├── scripts 79 | │ ├── seed 80 | │ ├── plugins 81 | │ ├── auth 82 | │ ├── public 83 | │ ├── assets 84 | │ ├── env.local 85 | │ ├── go.mod 86 | │ ├── go.sum 87 | │ ├── Makefile 88 | │ ├── package-lock.json 89 | │ ├── package.json 90 | │ ├── tailwind.config.js 91 | ├── db 92 | ├── event 93 | ├── kit 94 | │ ├── middleware 95 | ├── validate 96 | ├── view 97 | ├── go.mod 98 | ├── install.go 99 | ├── README.md 100 | ``` 101 | 102 | ### assets 103 | 104 | Assets are stored in the `assets` directory. This directory contains all your CSS and JavaScript files. The `assets` directory is structured as follows: 105 | 106 | ```bash 107 | assets 108 | ├── css 109 | │ ├── app.css 110 | ├── js 111 | │ ├── app.js 112 | ``` 113 | 114 | ### conf 115 | 116 | Configuration. First, config.yml is read, then environment variables overwrite the yaml config if they match. The config structure is in the config.go. The env-required: true tag obliges you to specify a value (either in yaml, or in environment variables). 117 | 118 | Reading the config from yaml contradicts the ideology of 12 factors, but in practice, it is more convenient than reading the entire config from ENV. It is assumed that default values are in yaml, and security-sensitive variables are defined in ENV. 119 | 120 | ### db 121 | 122 | The `db` directory contains all your database related files. The `db` directory is structured as follows: 123 | 124 | ```bash 125 | db 126 | ├── migrations 127 | │ ├── 20210919123456_create_users_table.sql 128 | ├── seeds 129 | │ ├── seed.go 130 | ``` 131 | 132 | ### events 133 | 134 | The `events` directory contains all your event related files. These files are used to define custom events and event handlers for the project. The `events` directory is structured as follows: 135 | 136 | ```bash 137 | events 138 | ├── event.go 139 | ``` 140 | 141 | ### handlers 142 | 143 | The `handlers` directory contains the main handlers or controllers for the project. These handlers handle incoming requests, perform necessary actions, and return appropriate responses. They encapsulate the business logic and interact with other components of the project, such as services and data repositories. 144 | 145 | It is important to note that the project structure described here may not include all the directories and files present in the actual project. The provided overview focuses on the key directories relevant to understanding the structure and organization of the project. 146 | 147 | ### types 148 | 149 | The `types` directory contains all your type related files. For example, you can define your models, structs, and interfaces in this directory. The `types` directory is structured as follows: 150 | 151 | ```bash 152 | types 153 | ├── user.go 154 | ├── auth.go 155 | ``` 156 | 157 | ### views 158 | 159 | The `views` directory contains all your view related files. These files are used to render HTML templates for the project. The `views` directory is structured as follows: 160 | 161 | ```bash 162 | views 163 | ├── home.go 164 | ├── about.go 165 | ``` 166 | 167 | ## Development server 168 | 169 | You can run the development server with the following command: 170 | 171 | ```bash 172 | make dev 173 | ``` 174 | 175 | ## Hot reloading the browser 176 | 177 | Hot reloading is configured by default when running your application in development. 178 | 179 | > NOTE: on windows or on in my case (WSL2) you might need to run `make watch-assets` in another terminal to watch for CSS and JS file changes. 180 | 181 | ## Migrations 182 | 183 | ### Create a new migration 184 | 185 | ```bash 186 | make db-mig-create add_users_table 187 | ``` 188 | 189 | The command will create a new migration SQL file located at `app/db/migrations/add_users_table.sql` 190 | 191 | ### Migrate the database 192 | 193 | ```bash 194 | make db-up 195 | ``` 196 | 197 | ### Reset the database 198 | 199 | ```bash 200 | make db-reset 201 | ``` 202 | 203 | ## Seeds 204 | 205 | ```bash 206 | make db-seed 207 | ``` 208 | 209 | This command will run the seed file located at `cmd/scripts/seed/main.go` 210 | 211 | ## Creating views with Templ 212 | 213 | superkit uses Templ as its templating engine. Templ allows you to create type safe view components that renders fragments of HTML. In-depth information about Templ can be found here: 214 | [Templ documentation](https://templ.guide) 215 | 216 | ## Validations 217 | 218 | todo 219 | 220 | ## Testing 221 | 222 | ### Testing handlers 223 | 224 | ## Create a production release 225 | 226 | superkit will compile your whole application including its assets into a single binary. To build your application for production you can run the following command: 227 | 228 | ```bash 229 | make build 230 | ``` 231 | 232 | This will create a binary file located at `/bin/app_prod`. 233 | 234 | Make sure you also set the correct application environment variable in your `.env` file. 235 | 236 | ```bash 237 | SUPERKIT_ENV = production 238 | ``` 239 | -------------------------------------------------------------------------------- /bootstrap/.env.local: -------------------------------------------------------------------------------- 1 | # Application environment 2 | # production or development 3 | SUPERKIT_ENV = development 4 | 5 | # HTTP listen port of the application 6 | HTTP_LISTEN_ADDR = :3000 7 | 8 | # Database configuration 9 | DB_DRIVER = sqlite3 10 | DB_USER = 11 | DB_HOST = 12 | DB_PASSWORD = 13 | DB_NAME = app_db 14 | 15 | MIGRATION_DIR = app/db/migrations 16 | 17 | # Application secret used to secure your sessions. 18 | # The secret will be auto generated on install. 19 | # If you still want to change it make sure its at 20 | # least 32 bytes long. 21 | # NOTE: You might want to change this secret when using 22 | # your app in production. 23 | SUPERKIT_SECRET = {{app_secret}} 24 | 25 | # Authentication Plugin 26 | SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN = /profile 27 | SUPERKIT_AUTH_SESSION_EXPIRY_IN_HOURS = 48 28 | # Skip user email verification after signup 29 | SUPERKIT_AUTH_SKIP_VERIFY = false 30 | SUPERKIT_AUTH_EMAIL_VERIFICATION_EXPIRY_IN_HOURS = 1 31 | -------------------------------------------------------------------------------- /bootstrap/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *_templ.go 3 | *_templ.txt 4 | node_modules 5 | tmp 6 | .env 7 | app_db -------------------------------------------------------------------------------- /bootstrap/Makefile: -------------------------------------------------------------------------------- 1 | # Load environment variables from .env file 2 | ifneq (,$(wildcard ./.env)) 3 | include .env 4 | export 5 | endif 6 | 7 | ifeq ($(OS),Windows_NT) 8 | MAIN_PATH = /tmp/bin/main.exe 9 | SYNC_ASSETS_COMMAND = @go run github.com/makiuchi-d/arelo@v1.13.1 \ 10 | --target "./public" \ 11 | --pattern "**/*.js" \ 12 | --pattern "**/*.css" \ 13 | --delay "100ms" \ 14 | --templ generate --notify-proxy 15 | else 16 | MAIN_PATH = tmp/bin/main 17 | SYNC_ASSETS_COMMAND = @go run github.com/cosmtrek/air@v1.51.0 \ 18 | --build.cmd "templ generate --notify-proxy" \ 19 | --build.bin "true" \ 20 | --build.delay "100" \ 21 | --build.exclude_dir "" \ 22 | --build.include_dir "public" \ 23 | --build.include_ext "js,css" \ 24 | --screen.clear_on_rebuild true \ 25 | --log.main_only true 26 | endif 27 | 28 | # run templ generation in watch mode to detect all .templ files and 29 | # re-create _templ.txt files on change, then send reload event to browser. 30 | # Default url: http://localhost:7331 31 | templ: 32 | @templ generate --watch --proxy="http://localhost$(HTTP_LISTEN_ADDR)" --open-browser=false 33 | 34 | # run air to detect any go file changes to re-build and re-run the server. 35 | server: 36 | @go run github.com/cosmtrek/air@v1.51.0 \ 37 | --build.cmd "go build --tags dev -o ${MAIN_PATH} ./cmd/app/" --build.bin "${MAIN_PATH}" --build.delay "100" \ 38 | --build.exclude_dir "node_modules" \ 39 | --build.include_ext "go" \ 40 | --build.stop_on_error "false" \ 41 | --misc.clean_on_exit true \ 42 | --screen.clear_on_rebuild true \ 43 | --log.main_only true 44 | 45 | # run tailwindcss to generate the styles.css bundle in watch mode. 46 | watch-assets: 47 | @npx tailwindcss -i app/assets/app.css -o ./public/assets/styles.css --watch 48 | 49 | # run esbuild to generate the index.js bundle in watch mode. 50 | watch-esbuild: 51 | @npx esbuild app/assets/index.js --bundle --outdir=public/assets --watch 52 | 53 | # watch for any js or css change in the assets/ folder, then reload the browser via templ proxy. 54 | sync_assets: 55 | ${SYNC_ASSETS_COMMAND} 56 | 57 | # start the application in development 58 | dev: 59 | @make -j5 templ server watch-assets watch-esbuild sync_assets 60 | 61 | # build the application for production. This will compile your app 62 | # to a single binary with all its assets embedded. 63 | build: 64 | @npx tailwindcss -i app/assets/app.css -o ./public/assets/styles.css 65 | @npx esbuild app/assets/index.js --bundle --outdir=public/assets 66 | @go build -o bin/app_prod cmd/app/main.go 67 | @echo "compiled you application with all its assets to a single binary => bin/app_prod" 68 | 69 | db-status: 70 | @GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) go run github.com/pressly/goose/v3/cmd/goose@latest -dir=$(MIGRATION_DIR) status 71 | 72 | db-reset: 73 | @GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) go run github.com/pressly/goose/v3/cmd/goose@latest -dir=$(MIGRATION_DIR) reset 74 | 75 | db-down: 76 | @GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) go run github.com/pressly/goose/v3/cmd/goose@latest -dir=$(MIGRATION_DIR) down 77 | 78 | db-up: 79 | @GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) go run github.com/pressly/goose/v3/cmd/goose@latest -dir=$(MIGRATION_DIR) up 80 | 81 | db-mig-create: 82 | @GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) go run github.com/pressly/goose/v3/cmd/goose@latest -dir=$(MIGRATION_DIR) create $(filter-out $@,$(MAKECMDGOALS)) sql 83 | 84 | db-seed: 85 | @go run cmd/scripts/seed/main.go 86 | -------------------------------------------------------------------------------- /bootstrap/app/assets/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* fix for Alpine users. */ 6 | [x-cloak] { 7 | display: none !important; 8 | } 9 | 10 | @layer base { 11 | * { 12 | @apply border-border; 13 | } 14 | body { 15 | @apply bg-background text-foreground; 16 | } 17 | 18 | @layer base { 19 | :root { 20 | --background: 0 0% 100%; 21 | --foreground: 240 10% 3.9%; 22 | --card: 0 0% 100%; 23 | --card-foreground: 240 10% 3.9%; 24 | --popover: 0 0% 100%;; 25 | --popover-foreground: 240 10% 3.9%; 26 | --primary: 240 5.9% 10%; 27 | --primary-foreground: 0 0% 98%; 28 | --secondary: 240 4.8% 95.9%; 29 | --secondary-foreground: 240 5.9% 10%; 30 | --muted: 240 4.8% 95.9%; 31 | --muted-foreground: 240 3.8% 46.1%; 32 | --accent: 240 4.8% 95.9%; 33 | --accent-foreground: 240 5.9% 10%; 34 | --destructive: 0 72.22% 50.59%; 35 | --destructive-foreground: 0 0% 98%; 36 | --border: 240 5.9% 90%; 37 | --input: 240 5.9% 90%; 38 | --ring: 240 5.9% 10%; 39 | --radius: 0.5rem; 40 | } 41 | 42 | .dark { 43 | --background: 240 10% 3.9%; 44 | --foreground: 0 0% 98%; 45 | --card: 240 10% 3.9%; 46 | --card-foreground: 0 0% 98%; 47 | --popover: 240 10% 3.9%; 48 | --popover-foreground: 0 0% 98%; 49 | --primary: 0 0% 98%; 50 | --primary-foreground: 240 5.9% 10%; 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | --muted: 240 3.7% 15.9%; 54 | --muted-foreground: 240 5% 64.9%; 55 | --accent: 240 3.7% 15.9%; 56 | --accent-foreground: 0 0% 98%; 57 | --destructive: 0 62.8% 30.6%; 58 | --destructive-foreground: 0 0% 98%; 59 | --border: 240 3.7% 15.9%; 60 | --input: 240 3.7% 15.9%; 61 | --ring: 240 4.9% 83.9%; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /bootstrap/app/assets/index.js: -------------------------------------------------------------------------------- 1 | console.log("if you like superkit consider given it a star on GitHub.") -------------------------------------------------------------------------------- /bootstrap/app/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | // Here goes your application config 4 | -------------------------------------------------------------------------------- /bootstrap/app/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/anthdm/superkit/db" 8 | 9 | _ "github.com/mattn/go-sqlite3" 10 | 11 | "gorm.io/driver/sqlite" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | // By default this is a pre-configured Gorm DB instance. 16 | // Change this type based on the database package of your likings. 17 | var dbInstance *gorm.DB 18 | 19 | // Get returns the instantiated DB instance. 20 | func Get() *gorm.DB { 21 | return dbInstance 22 | } 23 | 24 | func init() { 25 | // Create a default *sql.DB exposed by the superkit/db package 26 | // based on the given configuration. 27 | config := db.Config{ 28 | Driver: os.Getenv("DB_DRIVER"), 29 | Name: os.Getenv("DB_NAME"), 30 | Password: os.Getenv("DB_PASSWORD"), 31 | User: os.Getenv("DB_USER"), 32 | Host: os.Getenv("DB_HOST"), 33 | } 34 | dbinst, err := db.NewSQL(config) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | // Based on the driver create the corresponding DB instance. 39 | // By default, the SuperKit boilerplate comes with a pre-configured 40 | // ORM called Gorm. https://gorm.io. 41 | // 42 | // You can change this to any other DB interaction tool 43 | // of your liking. EG: 44 | // - uptrace bun -> https://bun.uptrace.dev 45 | // - SQLC -> https://github.com/sqlc-dev/sqlc 46 | // - gojet -> https://github.com/go-jet/jet 47 | switch config.Driver { 48 | case db.DriverSqlite3: 49 | dbInstance, err = gorm.Open(sqlite.New(sqlite.Config{ 50 | Conn: dbinst, 51 | })) 52 | case db.DriverMysql: 53 | // ... 54 | default: 55 | log.Fatal("invalid driver:", config.Driver) 56 | } 57 | if err != nil { 58 | log.Fatal(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /bootstrap/app/db/migrations/20240610161057_create_users_table.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | create table if not exists users( 3 | id integer primary key, 4 | email text unique not null, 5 | password_hash text not null, 6 | first_name text not null, 7 | last_name text not null, 8 | email_verified_at datetime, 9 | created_at datetime not null, 10 | updated_at datetime not null, 11 | deleted_at datetime 12 | ); 13 | 14 | -- +goose Down 15 | drop table if exists users; 16 | -------------------------------------------------------------------------------- /bootstrap/app/db/migrations/20240610163918_add_sessions_table.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | create table if not exists sessions( 3 | id integer primary key, 4 | token string not null, 5 | user_id integer not null references users, 6 | ip_address text, 7 | user_agent text, 8 | expires_at datetime not null, 9 | created_at datetime not null, 10 | updated_at datetime not null, 11 | deleted_at datetime 12 | ); 13 | 14 | -- +goose Down 15 | drop table if exists sessions; 16 | -------------------------------------------------------------------------------- /bootstrap/app/events.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "AABBCCDD/app/events" 5 | "AABBCCDD/plugins/auth" 6 | 7 | "github.com/anthdm/superkit/event" 8 | ) 9 | 10 | // Events are functions that are handled in separate goroutines. 11 | // They are the perfect fit for offloading work in your handlers 12 | // that otherwise would take up response time. 13 | // - sending email 14 | // - sending notifications (Slack, Telegram, Discord) 15 | // - analytics.. 16 | 17 | // Register your events here. 18 | func RegisterEvents() { 19 | event.Subscribe(auth.UserSignupEvent, events.OnUserSignup) 20 | event.Subscribe(auth.ResendVerificationEvent, events.OnResendVerificationToken) 21 | } 22 | -------------------------------------------------------------------------------- /bootstrap/app/events/auth.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "AABBCCDD/plugins/auth" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | ) 9 | 10 | // Event handlers 11 | func OnUserSignup(ctx context.Context, event any) { 12 | userWithToken, ok := event.(auth.UserWithVerificationToken) 13 | if !ok { 14 | return 15 | } 16 | b, _ := json.MarshalIndent(userWithToken, " ", " ") 17 | fmt.Println(string(b)) 18 | } 19 | 20 | func OnResendVerificationToken(ctx context.Context, event any) { 21 | userWithToken, ok := event.(auth.UserWithVerificationToken) 22 | if !ok { 23 | return 24 | } 25 | b, _ := json.MarshalIndent(userWithToken, " ", " ") 26 | fmt.Println(string(b)) 27 | } 28 | -------------------------------------------------------------------------------- /bootstrap/app/handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "AABBCCDD/app/types" 5 | 6 | "github.com/anthdm/superkit/kit" 7 | ) 8 | 9 | func HandleAuthentication(kit *kit.Kit) (kit.Auth, error) { 10 | return types.AuthUser{}, nil 11 | } 12 | -------------------------------------------------------------------------------- /bootstrap/app/handlers/landing.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "AABBCCDD/app/views/landing" 5 | 6 | "github.com/anthdm/superkit/kit" 7 | ) 8 | 9 | func HandleLandingIndex(kit *kit.Kit) error { 10 | return kit.Render(landing.Index()) 11 | } 12 | -------------------------------------------------------------------------------- /bootstrap/app/routes.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "AABBCCDD/app/handlers" 5 | "AABBCCDD/app/views/errors" 6 | "AABBCCDD/plugins/auth" 7 | "log/slog" 8 | 9 | "github.com/anthdm/superkit/kit" 10 | "github.com/anthdm/superkit/kit/middleware" 11 | "github.com/go-chi/chi/v5" 12 | 13 | chimiddleware "github.com/go-chi/chi/v5/middleware" 14 | ) 15 | 16 | // Define your global middleware 17 | func InitializeMiddleware(router *chi.Mux) { 18 | router.Use(chimiddleware.Logger) 19 | router.Use(chimiddleware.Recoverer) 20 | router.Use(middleware.WithRequest) 21 | } 22 | 23 | // Define your routes in here 24 | func InitializeRoutes(router *chi.Mux) { 25 | // Authentication plugin 26 | // 27 | // By default the auth plugin is active, to disable the auth plugin 28 | // you will need to pass your own handler in the `AuthFunc`` field 29 | // of the `kit.AuthenticationConfig`. 30 | // authConfig := kit.AuthenticationConfig{ 31 | // AuthFunc: YourAuthHandler, 32 | // RedirectURL: "/login", 33 | // } 34 | auth.InitializeRoutes(router) 35 | authConfig := kit.AuthenticationConfig{ 36 | AuthFunc: auth.AuthenticateUser, 37 | RedirectURL: "/login", 38 | } 39 | 40 | // Routes that "might" have an authenticated user 41 | router.Group(func(app chi.Router) { 42 | app.Use(kit.WithAuthentication(authConfig, false)) // strict set to false 43 | 44 | // Routes 45 | app.Get("/", kit.Handler(handlers.HandleLandingIndex)) 46 | }) 47 | 48 | // Authenticated routes 49 | // 50 | // Routes that "must" have an authenticated user or else they 51 | // will be redirected to the configured redirectURL, set in the 52 | // AuthenticationConfig. 53 | router.Group(func(app chi.Router) { 54 | app.Use(kit.WithAuthentication(authConfig, true)) // strict set to true 55 | 56 | // Routes 57 | // app.Get("/path", kit.Handler(myHandler.HandleIndex)) 58 | }) 59 | } 60 | 61 | // NotFoundHandler that will be called when the requested path could 62 | // not be found. 63 | func NotFoundHandler(kit *kit.Kit) error { 64 | return kit.Render(errors.Error404()) 65 | } 66 | 67 | // ErrorHandler that will be called on errors return from application handlers. 68 | func ErrorHandler(kit *kit.Kit, err error) { 69 | slog.Error("internal server error", "err", err.Error(), "path", kit.Request.URL.Path) 70 | kit.Render(errors.Error500()) 71 | } 72 | -------------------------------------------------------------------------------- /bootstrap/app/types/auth.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // AuthUser represents an user that might be authenticated. 4 | type AuthUser struct { 5 | ID uint 6 | Email string 7 | LoggedIn bool 8 | } 9 | 10 | // Check should return true if the user is authenticated. 11 | // See handlers/auth.go. 12 | func (user AuthUser) Check() bool { 13 | return user.ID > 0 && user.LoggedIn 14 | } 15 | -------------------------------------------------------------------------------- /bootstrap/app/views/components/navigation.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ Navigation() { 4 | 16 | } 17 | -------------------------------------------------------------------------------- /bootstrap/app/views/components/theme_switcher.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ ThemeSwitcher() { 4 |
5 | @LightIcon() 6 |
7 |
8 | @DarkIcon() 9 |
10 | } 11 | 12 | templ DarkIcon() { 13 | 14 | } 15 | 16 | templ LightIcon() { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /bootstrap/app/views/errors/404.templ: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "AABBCCDD/app/views/layouts" 5 | ) 6 | 7 | templ Error404() { 8 | @layouts.BaseLayout() { 9 |
10 |
404
11 |
The page you are looking for does not exist
12 |
13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bootstrap/app/views/errors/500.templ: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "AABBCCDD/app/views/layouts" 4 | 5 | templ Error500() { 6 | @layouts.BaseLayout() { 7 |
8 |
500
9 |
An unexpected error occured
10 |
11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bootstrap/app/views/landing/index.templ: -------------------------------------------------------------------------------- 1 | package landing 2 | 3 | import ( 4 | "AABBCCDD/app/views/layouts" 5 | ) 6 | 7 | templ Index() { 8 | @layouts.App() { 9 |
10 |
11 |

superkit

12 |
13 |

14 | Build high-performance apps swiftly with minimal team resources in GO. 15 |

16 |

Escape the JavaScript ecosystem

17 |
18 | 21 |
22 |
23 | } 24 | } 25 | -------------------------------------------------------------------------------- /bootstrap/app/views/layouts/app_layout.templ: -------------------------------------------------------------------------------- 1 | package layouts 2 | 3 | import "AABBCCDD/app/views/components" 4 | 5 | templ App() { 6 | @BaseLayout() { 7 | @components.Navigation() 8 |
9 | { children... } 10 |
11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bootstrap/app/views/layouts/base_layout.templ: -------------------------------------------------------------------------------- 1 | package layouts 2 | 3 | import "github.com/anthdm/superkit/view" 4 | 5 | var ( 6 | title = "superkit project" 7 | ) 8 | 9 | templ BaseLayout() { 10 | 11 | 12 | 13 | { title } 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | { children... } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /bootstrap/cmd/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "AABBCCDD/app" 5 | "AABBCCDD/public" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | 11 | "github.com/anthdm/superkit/kit" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/joho/godotenv" 14 | ) 15 | 16 | func main() { 17 | kit.Setup() 18 | router := chi.NewMux() 19 | 20 | app.InitializeMiddleware(router) 21 | 22 | if kit.IsDevelopment() { 23 | router.Handle("/public/*", disableCache(staticDev())) 24 | } else if kit.IsProduction() { 25 | router.Handle("/public/*", staticProd()) 26 | } 27 | 28 | kit.UseErrorHandler(app.ErrorHandler) 29 | router.HandleFunc("/*", kit.Handler(app.NotFoundHandler)) 30 | 31 | app.InitializeRoutes(router) 32 | app.RegisterEvents() 33 | 34 | listenAddr := os.Getenv("HTTP_LISTEN_ADDR") 35 | // In development link the full Templ proxy url. 36 | url := "http://localhost:7331" 37 | if kit.IsProduction() { 38 | url = fmt.Sprintf("http://localhost%s", listenAddr) 39 | } 40 | 41 | fmt.Printf("application running in %s at %s\n", kit.Env(), url) 42 | 43 | http.ListenAndServe(listenAddr, router) 44 | } 45 | 46 | func staticDev() http.Handler { 47 | return http.StripPrefix("/public/", http.FileServerFS(os.DirFS("public"))) 48 | } 49 | 50 | func staticProd() http.Handler { 51 | return http.StripPrefix("/public/", http.FileServerFS(public.AssetsFS)) 52 | } 53 | 54 | func disableCache(next http.Handler) http.Handler { 55 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | w.Header().Set("Cache-Control", "no-store") 57 | next.ServeHTTP(w, r) 58 | }) 59 | } 60 | 61 | func init() { 62 | if err := godotenv.Load(); err != nil { 63 | log.Fatal(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /bootstrap/cmd/scripts/seed/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("there are no seeds.") 7 | } 8 | -------------------------------------------------------------------------------- /bootstrap/go.mod: -------------------------------------------------------------------------------- 1 | module AABBCCDD 2 | 3 | go 1.22.4 4 | 5 | // uncomment for local development on the superkit core. 6 | // replace github.com/anthdm/superkit => ../ 7 | 8 | require ( 9 | github.com/a-h/templ v0.2.731 10 | github.com/anthdm/superkit v0.0.0-20240622052611-30be5bb82e0d 11 | github.com/go-chi/chi/v5 v5.0.14 12 | github.com/golang-jwt/jwt/v5 v5.2.1 13 | github.com/google/uuid v1.6.0 14 | github.com/joho/godotenv v1.5.1 15 | github.com/mattn/go-sqlite3 v1.14.22 16 | golang.org/x/crypto v0.24.0 17 | gorm.io/driver/sqlite v1.5.6 18 | gorm.io/gorm v1.25.10 19 | ) 20 | 21 | require ( 22 | github.com/gorilla/securecookie v1.1.2 // indirect 23 | github.com/gorilla/sessions v1.3.0 // indirect 24 | github.com/jinzhu/inflection v1.0.0 // indirect 25 | github.com/jinzhu/now v1.1.5 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /bootstrap/go.sum: -------------------------------------------------------------------------------- 1 | github.com/a-h/templ v0.2.731 h1:yiv4C7whSUsa36y65O06DPr/U/j3+WGB0RmvLOoVFXc= 2 | github.com/a-h/templ v0.2.731/go.mod h1:IejA/ecDD0ul0dCvgCwp9t7bUZXVpGClEAdsqZQigi8= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0= 6 | github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 7 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 8 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 12 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 13 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 14 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 15 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 16 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 17 | github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= 18 | github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= 19 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 20 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 21 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 22 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 23 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 24 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 25 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 26 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 30 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 31 | golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= 32 | golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= 36 | gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= 37 | gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= 38 | gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 39 | -------------------------------------------------------------------------------- /bootstrap/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superkit", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "superkit", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "esbuild": "^0.21.5" 13 | }, 14 | "devDependencies": { 15 | "tailwindcss": "^3.4.3" 16 | } 17 | }, 18 | "node_modules/@alloc/quick-lru": { 19 | "version": "5.2.0", 20 | "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 21 | "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 22 | "dev": true, 23 | "engines": { 24 | "node": ">=10" 25 | }, 26 | "funding": { 27 | "url": "https://github.com/sponsors/sindresorhus" 28 | } 29 | }, 30 | "node_modules/@esbuild/aix-ppc64": { 31 | "version": "0.21.5", 32 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", 33 | "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", 34 | "cpu": [ 35 | "ppc64" 36 | ], 37 | "license": "MIT", 38 | "optional": true, 39 | "os": [ 40 | "aix" 41 | ], 42 | "engines": { 43 | "node": ">=12" 44 | } 45 | }, 46 | "node_modules/@esbuild/android-arm": { 47 | "version": "0.21.5", 48 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", 49 | "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", 50 | "cpu": [ 51 | "arm" 52 | ], 53 | "license": "MIT", 54 | "optional": true, 55 | "os": [ 56 | "android" 57 | ], 58 | "engines": { 59 | "node": ">=12" 60 | } 61 | }, 62 | "node_modules/@esbuild/android-arm64": { 63 | "version": "0.21.5", 64 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", 65 | "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", 66 | "cpu": [ 67 | "arm64" 68 | ], 69 | "license": "MIT", 70 | "optional": true, 71 | "os": [ 72 | "android" 73 | ], 74 | "engines": { 75 | "node": ">=12" 76 | } 77 | }, 78 | "node_modules/@esbuild/android-x64": { 79 | "version": "0.21.5", 80 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", 81 | "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", 82 | "cpu": [ 83 | "x64" 84 | ], 85 | "license": "MIT", 86 | "optional": true, 87 | "os": [ 88 | "android" 89 | ], 90 | "engines": { 91 | "node": ">=12" 92 | } 93 | }, 94 | "node_modules/@esbuild/darwin-arm64": { 95 | "version": "0.21.5", 96 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", 97 | "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", 98 | "cpu": [ 99 | "arm64" 100 | ], 101 | "license": "MIT", 102 | "optional": true, 103 | "os": [ 104 | "darwin" 105 | ], 106 | "engines": { 107 | "node": ">=12" 108 | } 109 | }, 110 | "node_modules/@esbuild/darwin-x64": { 111 | "version": "0.21.5", 112 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", 113 | "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", 114 | "cpu": [ 115 | "x64" 116 | ], 117 | "license": "MIT", 118 | "optional": true, 119 | "os": [ 120 | "darwin" 121 | ], 122 | "engines": { 123 | "node": ">=12" 124 | } 125 | }, 126 | "node_modules/@esbuild/freebsd-arm64": { 127 | "version": "0.21.5", 128 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", 129 | "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", 130 | "cpu": [ 131 | "arm64" 132 | ], 133 | "license": "MIT", 134 | "optional": true, 135 | "os": [ 136 | "freebsd" 137 | ], 138 | "engines": { 139 | "node": ">=12" 140 | } 141 | }, 142 | "node_modules/@esbuild/freebsd-x64": { 143 | "version": "0.21.5", 144 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", 145 | "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", 146 | "cpu": [ 147 | "x64" 148 | ], 149 | "license": "MIT", 150 | "optional": true, 151 | "os": [ 152 | "freebsd" 153 | ], 154 | "engines": { 155 | "node": ">=12" 156 | } 157 | }, 158 | "node_modules/@esbuild/linux-arm": { 159 | "version": "0.21.5", 160 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", 161 | "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", 162 | "cpu": [ 163 | "arm" 164 | ], 165 | "license": "MIT", 166 | "optional": true, 167 | "os": [ 168 | "linux" 169 | ], 170 | "engines": { 171 | "node": ">=12" 172 | } 173 | }, 174 | "node_modules/@esbuild/linux-arm64": { 175 | "version": "0.21.5", 176 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", 177 | "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", 178 | "cpu": [ 179 | "arm64" 180 | ], 181 | "license": "MIT", 182 | "optional": true, 183 | "os": [ 184 | "linux" 185 | ], 186 | "engines": { 187 | "node": ">=12" 188 | } 189 | }, 190 | "node_modules/@esbuild/linux-ia32": { 191 | "version": "0.21.5", 192 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", 193 | "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", 194 | "cpu": [ 195 | "ia32" 196 | ], 197 | "license": "MIT", 198 | "optional": true, 199 | "os": [ 200 | "linux" 201 | ], 202 | "engines": { 203 | "node": ">=12" 204 | } 205 | }, 206 | "node_modules/@esbuild/linux-loong64": { 207 | "version": "0.21.5", 208 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", 209 | "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", 210 | "cpu": [ 211 | "loong64" 212 | ], 213 | "license": "MIT", 214 | "optional": true, 215 | "os": [ 216 | "linux" 217 | ], 218 | "engines": { 219 | "node": ">=12" 220 | } 221 | }, 222 | "node_modules/@esbuild/linux-mips64el": { 223 | "version": "0.21.5", 224 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", 225 | "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", 226 | "cpu": [ 227 | "mips64el" 228 | ], 229 | "license": "MIT", 230 | "optional": true, 231 | "os": [ 232 | "linux" 233 | ], 234 | "engines": { 235 | "node": ">=12" 236 | } 237 | }, 238 | "node_modules/@esbuild/linux-ppc64": { 239 | "version": "0.21.5", 240 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", 241 | "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", 242 | "cpu": [ 243 | "ppc64" 244 | ], 245 | "license": "MIT", 246 | "optional": true, 247 | "os": [ 248 | "linux" 249 | ], 250 | "engines": { 251 | "node": ">=12" 252 | } 253 | }, 254 | "node_modules/@esbuild/linux-riscv64": { 255 | "version": "0.21.5", 256 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", 257 | "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", 258 | "cpu": [ 259 | "riscv64" 260 | ], 261 | "license": "MIT", 262 | "optional": true, 263 | "os": [ 264 | "linux" 265 | ], 266 | "engines": { 267 | "node": ">=12" 268 | } 269 | }, 270 | "node_modules/@esbuild/linux-s390x": { 271 | "version": "0.21.5", 272 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", 273 | "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", 274 | "cpu": [ 275 | "s390x" 276 | ], 277 | "license": "MIT", 278 | "optional": true, 279 | "os": [ 280 | "linux" 281 | ], 282 | "engines": { 283 | "node": ">=12" 284 | } 285 | }, 286 | "node_modules/@esbuild/linux-x64": { 287 | "version": "0.21.5", 288 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", 289 | "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", 290 | "cpu": [ 291 | "x64" 292 | ], 293 | "license": "MIT", 294 | "optional": true, 295 | "os": [ 296 | "linux" 297 | ], 298 | "engines": { 299 | "node": ">=12" 300 | } 301 | }, 302 | "node_modules/@esbuild/netbsd-x64": { 303 | "version": "0.21.5", 304 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", 305 | "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", 306 | "cpu": [ 307 | "x64" 308 | ], 309 | "license": "MIT", 310 | "optional": true, 311 | "os": [ 312 | "netbsd" 313 | ], 314 | "engines": { 315 | "node": ">=12" 316 | } 317 | }, 318 | "node_modules/@esbuild/openbsd-x64": { 319 | "version": "0.21.5", 320 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", 321 | "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", 322 | "cpu": [ 323 | "x64" 324 | ], 325 | "license": "MIT", 326 | "optional": true, 327 | "os": [ 328 | "openbsd" 329 | ], 330 | "engines": { 331 | "node": ">=12" 332 | } 333 | }, 334 | "node_modules/@esbuild/sunos-x64": { 335 | "version": "0.21.5", 336 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", 337 | "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", 338 | "cpu": [ 339 | "x64" 340 | ], 341 | "license": "MIT", 342 | "optional": true, 343 | "os": [ 344 | "sunos" 345 | ], 346 | "engines": { 347 | "node": ">=12" 348 | } 349 | }, 350 | "node_modules/@esbuild/win32-arm64": { 351 | "version": "0.21.5", 352 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", 353 | "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", 354 | "cpu": [ 355 | "arm64" 356 | ], 357 | "license": "MIT", 358 | "optional": true, 359 | "os": [ 360 | "win32" 361 | ], 362 | "engines": { 363 | "node": ">=12" 364 | } 365 | }, 366 | "node_modules/@esbuild/win32-ia32": { 367 | "version": "0.21.5", 368 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", 369 | "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", 370 | "cpu": [ 371 | "ia32" 372 | ], 373 | "license": "MIT", 374 | "optional": true, 375 | "os": [ 376 | "win32" 377 | ], 378 | "engines": { 379 | "node": ">=12" 380 | } 381 | }, 382 | "node_modules/@esbuild/win32-x64": { 383 | "version": "0.21.5", 384 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", 385 | "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", 386 | "cpu": [ 387 | "x64" 388 | ], 389 | "license": "MIT", 390 | "optional": true, 391 | "os": [ 392 | "win32" 393 | ], 394 | "engines": { 395 | "node": ">=12" 396 | } 397 | }, 398 | "node_modules/@isaacs/cliui": { 399 | "version": "8.0.2", 400 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 401 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 402 | "dev": true, 403 | "dependencies": { 404 | "string-width": "^5.1.2", 405 | "string-width-cjs": "npm:string-width@^4.2.0", 406 | "strip-ansi": "^7.0.1", 407 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 408 | "wrap-ansi": "^8.1.0", 409 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 410 | }, 411 | "engines": { 412 | "node": ">=12" 413 | } 414 | }, 415 | "node_modules/@jridgewell/gen-mapping": { 416 | "version": "0.3.5", 417 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", 418 | "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", 419 | "dev": true, 420 | "dependencies": { 421 | "@jridgewell/set-array": "^1.2.1", 422 | "@jridgewell/sourcemap-codec": "^1.4.10", 423 | "@jridgewell/trace-mapping": "^0.3.24" 424 | }, 425 | "engines": { 426 | "node": ">=6.0.0" 427 | } 428 | }, 429 | "node_modules/@jridgewell/resolve-uri": { 430 | "version": "3.1.2", 431 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 432 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 433 | "dev": true, 434 | "engines": { 435 | "node": ">=6.0.0" 436 | } 437 | }, 438 | "node_modules/@jridgewell/set-array": { 439 | "version": "1.2.1", 440 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 441 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 442 | "dev": true, 443 | "engines": { 444 | "node": ">=6.0.0" 445 | } 446 | }, 447 | "node_modules/@jridgewell/sourcemap-codec": { 448 | "version": "1.4.15", 449 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 450 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 451 | "dev": true 452 | }, 453 | "node_modules/@jridgewell/trace-mapping": { 454 | "version": "0.3.25", 455 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 456 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 457 | "dev": true, 458 | "dependencies": { 459 | "@jridgewell/resolve-uri": "^3.1.0", 460 | "@jridgewell/sourcemap-codec": "^1.4.14" 461 | } 462 | }, 463 | "node_modules/@nodelib/fs.scandir": { 464 | "version": "2.1.5", 465 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 466 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 467 | "dev": true, 468 | "dependencies": { 469 | "@nodelib/fs.stat": "2.0.5", 470 | "run-parallel": "^1.1.9" 471 | }, 472 | "engines": { 473 | "node": ">= 8" 474 | } 475 | }, 476 | "node_modules/@nodelib/fs.stat": { 477 | "version": "2.0.5", 478 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 479 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 480 | "dev": true, 481 | "engines": { 482 | "node": ">= 8" 483 | } 484 | }, 485 | "node_modules/@nodelib/fs.walk": { 486 | "version": "1.2.8", 487 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 488 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 489 | "dev": true, 490 | "dependencies": { 491 | "@nodelib/fs.scandir": "2.1.5", 492 | "fastq": "^1.6.0" 493 | }, 494 | "engines": { 495 | "node": ">= 8" 496 | } 497 | }, 498 | "node_modules/@pkgjs/parseargs": { 499 | "version": "0.11.0", 500 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 501 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 502 | "dev": true, 503 | "optional": true, 504 | "engines": { 505 | "node": ">=14" 506 | } 507 | }, 508 | "node_modules/ansi-regex": { 509 | "version": "6.0.1", 510 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 511 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", 512 | "dev": true, 513 | "engines": { 514 | "node": ">=12" 515 | }, 516 | "funding": { 517 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 518 | } 519 | }, 520 | "node_modules/ansi-styles": { 521 | "version": "6.2.1", 522 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 523 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 524 | "dev": true, 525 | "engines": { 526 | "node": ">=12" 527 | }, 528 | "funding": { 529 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 530 | } 531 | }, 532 | "node_modules/any-promise": { 533 | "version": "1.3.0", 534 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 535 | "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", 536 | "dev": true 537 | }, 538 | "node_modules/anymatch": { 539 | "version": "3.1.3", 540 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 541 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 542 | "dev": true, 543 | "dependencies": { 544 | "normalize-path": "^3.0.0", 545 | "picomatch": "^2.0.4" 546 | }, 547 | "engines": { 548 | "node": ">= 8" 549 | } 550 | }, 551 | "node_modules/arg": { 552 | "version": "5.0.2", 553 | "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", 554 | "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", 555 | "dev": true 556 | }, 557 | "node_modules/balanced-match": { 558 | "version": "1.0.2", 559 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 560 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 561 | "dev": true 562 | }, 563 | "node_modules/binary-extensions": { 564 | "version": "2.3.0", 565 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 566 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 567 | "dev": true, 568 | "engines": { 569 | "node": ">=8" 570 | }, 571 | "funding": { 572 | "url": "https://github.com/sponsors/sindresorhus" 573 | } 574 | }, 575 | "node_modules/brace-expansion": { 576 | "version": "2.0.1", 577 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 578 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 579 | "dev": true, 580 | "dependencies": { 581 | "balanced-match": "^1.0.0" 582 | } 583 | }, 584 | "node_modules/braces": { 585 | "version": "3.0.3", 586 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 587 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 588 | "dev": true, 589 | "dependencies": { 590 | "fill-range": "^7.1.1" 591 | }, 592 | "engines": { 593 | "node": ">=8" 594 | } 595 | }, 596 | "node_modules/camelcase-css": { 597 | "version": "2.0.1", 598 | "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", 599 | "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", 600 | "dev": true, 601 | "engines": { 602 | "node": ">= 6" 603 | } 604 | }, 605 | "node_modules/chokidar": { 606 | "version": "3.6.0", 607 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 608 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 609 | "dev": true, 610 | "dependencies": { 611 | "anymatch": "~3.1.2", 612 | "braces": "~3.0.2", 613 | "glob-parent": "~5.1.2", 614 | "is-binary-path": "~2.1.0", 615 | "is-glob": "~4.0.1", 616 | "normalize-path": "~3.0.0", 617 | "readdirp": "~3.6.0" 618 | }, 619 | "engines": { 620 | "node": ">= 8.10.0" 621 | }, 622 | "funding": { 623 | "url": "https://paulmillr.com/funding/" 624 | }, 625 | "optionalDependencies": { 626 | "fsevents": "~2.3.2" 627 | } 628 | }, 629 | "node_modules/chokidar/node_modules/glob-parent": { 630 | "version": "5.1.2", 631 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 632 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 633 | "dev": true, 634 | "dependencies": { 635 | "is-glob": "^4.0.1" 636 | }, 637 | "engines": { 638 | "node": ">= 6" 639 | } 640 | }, 641 | "node_modules/color-convert": { 642 | "version": "2.0.1", 643 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 644 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 645 | "dev": true, 646 | "dependencies": { 647 | "color-name": "~1.1.4" 648 | }, 649 | "engines": { 650 | "node": ">=7.0.0" 651 | } 652 | }, 653 | "node_modules/color-name": { 654 | "version": "1.1.4", 655 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 656 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 657 | "dev": true 658 | }, 659 | "node_modules/commander": { 660 | "version": "4.1.1", 661 | "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 662 | "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 663 | "dev": true, 664 | "engines": { 665 | "node": ">= 6" 666 | } 667 | }, 668 | "node_modules/cross-spawn": { 669 | "version": "7.0.3", 670 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 671 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 672 | "dev": true, 673 | "dependencies": { 674 | "path-key": "^3.1.0", 675 | "shebang-command": "^2.0.0", 676 | "which": "^2.0.1" 677 | }, 678 | "engines": { 679 | "node": ">= 8" 680 | } 681 | }, 682 | "node_modules/cssesc": { 683 | "version": "3.0.0", 684 | "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", 685 | "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", 686 | "dev": true, 687 | "bin": { 688 | "cssesc": "bin/cssesc" 689 | }, 690 | "engines": { 691 | "node": ">=4" 692 | } 693 | }, 694 | "node_modules/didyoumean": { 695 | "version": "1.2.2", 696 | "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", 697 | "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", 698 | "dev": true 699 | }, 700 | "node_modules/dlv": { 701 | "version": "1.1.3", 702 | "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", 703 | "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", 704 | "dev": true 705 | }, 706 | "node_modules/eastasianwidth": { 707 | "version": "0.2.0", 708 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 709 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 710 | "dev": true 711 | }, 712 | "node_modules/emoji-regex": { 713 | "version": "9.2.2", 714 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 715 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 716 | "dev": true 717 | }, 718 | "node_modules/esbuild": { 719 | "version": "0.21.5", 720 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", 721 | "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", 722 | "hasInstallScript": true, 723 | "license": "MIT", 724 | "bin": { 725 | "esbuild": "bin/esbuild" 726 | }, 727 | "engines": { 728 | "node": ">=12" 729 | }, 730 | "optionalDependencies": { 731 | "@esbuild/aix-ppc64": "0.21.5", 732 | "@esbuild/android-arm": "0.21.5", 733 | "@esbuild/android-arm64": "0.21.5", 734 | "@esbuild/android-x64": "0.21.5", 735 | "@esbuild/darwin-arm64": "0.21.5", 736 | "@esbuild/darwin-x64": "0.21.5", 737 | "@esbuild/freebsd-arm64": "0.21.5", 738 | "@esbuild/freebsd-x64": "0.21.5", 739 | "@esbuild/linux-arm": "0.21.5", 740 | "@esbuild/linux-arm64": "0.21.5", 741 | "@esbuild/linux-ia32": "0.21.5", 742 | "@esbuild/linux-loong64": "0.21.5", 743 | "@esbuild/linux-mips64el": "0.21.5", 744 | "@esbuild/linux-ppc64": "0.21.5", 745 | "@esbuild/linux-riscv64": "0.21.5", 746 | "@esbuild/linux-s390x": "0.21.5", 747 | "@esbuild/linux-x64": "0.21.5", 748 | "@esbuild/netbsd-x64": "0.21.5", 749 | "@esbuild/openbsd-x64": "0.21.5", 750 | "@esbuild/sunos-x64": "0.21.5", 751 | "@esbuild/win32-arm64": "0.21.5", 752 | "@esbuild/win32-ia32": "0.21.5", 753 | "@esbuild/win32-x64": "0.21.5" 754 | } 755 | }, 756 | "node_modules/fast-glob": { 757 | "version": "3.3.2", 758 | "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", 759 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 760 | "dev": true, 761 | "dependencies": { 762 | "@nodelib/fs.stat": "^2.0.2", 763 | "@nodelib/fs.walk": "^1.2.3", 764 | "glob-parent": "^5.1.2", 765 | "merge2": "^1.3.0", 766 | "micromatch": "^4.0.4" 767 | }, 768 | "engines": { 769 | "node": ">=8.6.0" 770 | } 771 | }, 772 | "node_modules/fast-glob/node_modules/glob-parent": { 773 | "version": "5.1.2", 774 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 775 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 776 | "dev": true, 777 | "dependencies": { 778 | "is-glob": "^4.0.1" 779 | }, 780 | "engines": { 781 | "node": ">= 6" 782 | } 783 | }, 784 | "node_modules/fastq": { 785 | "version": "1.17.1", 786 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", 787 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 788 | "dev": true, 789 | "dependencies": { 790 | "reusify": "^1.0.4" 791 | } 792 | }, 793 | "node_modules/fill-range": { 794 | "version": "7.1.1", 795 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 796 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 797 | "dev": true, 798 | "dependencies": { 799 | "to-regex-range": "^5.0.1" 800 | }, 801 | "engines": { 802 | "node": ">=8" 803 | } 804 | }, 805 | "node_modules/foreground-child": { 806 | "version": "3.1.1", 807 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", 808 | "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", 809 | "dev": true, 810 | "dependencies": { 811 | "cross-spawn": "^7.0.0", 812 | "signal-exit": "^4.0.1" 813 | }, 814 | "engines": { 815 | "node": ">=14" 816 | }, 817 | "funding": { 818 | "url": "https://github.com/sponsors/isaacs" 819 | } 820 | }, 821 | "node_modules/fsevents": { 822 | "version": "2.3.3", 823 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 824 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 825 | "dev": true, 826 | "hasInstallScript": true, 827 | "optional": true, 828 | "os": [ 829 | "darwin" 830 | ], 831 | "engines": { 832 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 833 | } 834 | }, 835 | "node_modules/function-bind": { 836 | "version": "1.1.2", 837 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 838 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 839 | "dev": true, 840 | "funding": { 841 | "url": "https://github.com/sponsors/ljharb" 842 | } 843 | }, 844 | "node_modules/glob": { 845 | "version": "10.4.1", 846 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", 847 | "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", 848 | "dev": true, 849 | "dependencies": { 850 | "foreground-child": "^3.1.0", 851 | "jackspeak": "^3.1.2", 852 | "minimatch": "^9.0.4", 853 | "minipass": "^7.1.2", 854 | "path-scurry": "^1.11.1" 855 | }, 856 | "bin": { 857 | "glob": "dist/esm/bin.mjs" 858 | }, 859 | "engines": { 860 | "node": ">=16 || 14 >=14.18" 861 | }, 862 | "funding": { 863 | "url": "https://github.com/sponsors/isaacs" 864 | } 865 | }, 866 | "node_modules/glob-parent": { 867 | "version": "6.0.2", 868 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 869 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 870 | "dev": true, 871 | "dependencies": { 872 | "is-glob": "^4.0.3" 873 | }, 874 | "engines": { 875 | "node": ">=10.13.0" 876 | } 877 | }, 878 | "node_modules/hasown": { 879 | "version": "2.0.2", 880 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 881 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 882 | "dev": true, 883 | "dependencies": { 884 | "function-bind": "^1.1.2" 885 | }, 886 | "engines": { 887 | "node": ">= 0.4" 888 | } 889 | }, 890 | "node_modules/is-binary-path": { 891 | "version": "2.1.0", 892 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 893 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 894 | "dev": true, 895 | "dependencies": { 896 | "binary-extensions": "^2.0.0" 897 | }, 898 | "engines": { 899 | "node": ">=8" 900 | } 901 | }, 902 | "node_modules/is-core-module": { 903 | "version": "2.13.1", 904 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", 905 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", 906 | "dev": true, 907 | "dependencies": { 908 | "hasown": "^2.0.0" 909 | }, 910 | "funding": { 911 | "url": "https://github.com/sponsors/ljharb" 912 | } 913 | }, 914 | "node_modules/is-extglob": { 915 | "version": "2.1.1", 916 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 917 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 918 | "dev": true, 919 | "engines": { 920 | "node": ">=0.10.0" 921 | } 922 | }, 923 | "node_modules/is-fullwidth-code-point": { 924 | "version": "3.0.0", 925 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 926 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 927 | "dev": true, 928 | "engines": { 929 | "node": ">=8" 930 | } 931 | }, 932 | "node_modules/is-glob": { 933 | "version": "4.0.3", 934 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 935 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 936 | "dev": true, 937 | "dependencies": { 938 | "is-extglob": "^2.1.1" 939 | }, 940 | "engines": { 941 | "node": ">=0.10.0" 942 | } 943 | }, 944 | "node_modules/is-number": { 945 | "version": "7.0.0", 946 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 947 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 948 | "dev": true, 949 | "engines": { 950 | "node": ">=0.12.0" 951 | } 952 | }, 953 | "node_modules/isexe": { 954 | "version": "2.0.0", 955 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 956 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 957 | "dev": true 958 | }, 959 | "node_modules/jackspeak": { 960 | "version": "3.1.2", 961 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", 962 | "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", 963 | "dev": true, 964 | "dependencies": { 965 | "@isaacs/cliui": "^8.0.2" 966 | }, 967 | "engines": { 968 | "node": ">=14" 969 | }, 970 | "funding": { 971 | "url": "https://github.com/sponsors/isaacs" 972 | }, 973 | "optionalDependencies": { 974 | "@pkgjs/parseargs": "^0.11.0" 975 | } 976 | }, 977 | "node_modules/jiti": { 978 | "version": "1.21.0", 979 | "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", 980 | "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", 981 | "dev": true, 982 | "bin": { 983 | "jiti": "bin/jiti.js" 984 | } 985 | }, 986 | "node_modules/lilconfig": { 987 | "version": "2.1.0", 988 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", 989 | "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", 990 | "dev": true, 991 | "engines": { 992 | "node": ">=10" 993 | } 994 | }, 995 | "node_modules/lines-and-columns": { 996 | "version": "1.2.4", 997 | "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 998 | "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", 999 | "dev": true 1000 | }, 1001 | "node_modules/lru-cache": { 1002 | "version": "10.2.2", 1003 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", 1004 | "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", 1005 | "dev": true, 1006 | "engines": { 1007 | "node": "14 || >=16.14" 1008 | } 1009 | }, 1010 | "node_modules/merge2": { 1011 | "version": "1.4.1", 1012 | "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 1013 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 1014 | "dev": true, 1015 | "engines": { 1016 | "node": ">= 8" 1017 | } 1018 | }, 1019 | "node_modules/micromatch": { 1020 | "version": "4.0.7", 1021 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", 1022 | "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", 1023 | "dev": true, 1024 | "dependencies": { 1025 | "braces": "^3.0.3", 1026 | "picomatch": "^2.3.1" 1027 | }, 1028 | "engines": { 1029 | "node": ">=8.6" 1030 | } 1031 | }, 1032 | "node_modules/minimatch": { 1033 | "version": "9.0.4", 1034 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", 1035 | "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", 1036 | "dev": true, 1037 | "dependencies": { 1038 | "brace-expansion": "^2.0.1" 1039 | }, 1040 | "engines": { 1041 | "node": ">=16 || 14 >=14.17" 1042 | }, 1043 | "funding": { 1044 | "url": "https://github.com/sponsors/isaacs" 1045 | } 1046 | }, 1047 | "node_modules/minipass": { 1048 | "version": "7.1.2", 1049 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 1050 | "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 1051 | "dev": true, 1052 | "engines": { 1053 | "node": ">=16 || 14 >=14.17" 1054 | } 1055 | }, 1056 | "node_modules/mz": { 1057 | "version": "2.7.0", 1058 | "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 1059 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 1060 | "dev": true, 1061 | "dependencies": { 1062 | "any-promise": "^1.0.0", 1063 | "object-assign": "^4.0.1", 1064 | "thenify-all": "^1.0.0" 1065 | } 1066 | }, 1067 | "node_modules/nanoid": { 1068 | "version": "3.3.7", 1069 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 1070 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 1071 | "dev": true, 1072 | "funding": [ 1073 | { 1074 | "type": "github", 1075 | "url": "https://github.com/sponsors/ai" 1076 | } 1077 | ], 1078 | "bin": { 1079 | "nanoid": "bin/nanoid.cjs" 1080 | }, 1081 | "engines": { 1082 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1083 | } 1084 | }, 1085 | "node_modules/normalize-path": { 1086 | "version": "3.0.0", 1087 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1088 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1089 | "dev": true, 1090 | "engines": { 1091 | "node": ">=0.10.0" 1092 | } 1093 | }, 1094 | "node_modules/object-assign": { 1095 | "version": "4.1.1", 1096 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1097 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1098 | "dev": true, 1099 | "engines": { 1100 | "node": ">=0.10.0" 1101 | } 1102 | }, 1103 | "node_modules/object-hash": { 1104 | "version": "3.0.0", 1105 | "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", 1106 | "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", 1107 | "dev": true, 1108 | "engines": { 1109 | "node": ">= 6" 1110 | } 1111 | }, 1112 | "node_modules/path-key": { 1113 | "version": "3.1.1", 1114 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1115 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1116 | "dev": true, 1117 | "engines": { 1118 | "node": ">=8" 1119 | } 1120 | }, 1121 | "node_modules/path-parse": { 1122 | "version": "1.0.7", 1123 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 1124 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 1125 | "dev": true 1126 | }, 1127 | "node_modules/path-scurry": { 1128 | "version": "1.11.1", 1129 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 1130 | "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 1131 | "dev": true, 1132 | "dependencies": { 1133 | "lru-cache": "^10.2.0", 1134 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 1135 | }, 1136 | "engines": { 1137 | "node": ">=16 || 14 >=14.18" 1138 | }, 1139 | "funding": { 1140 | "url": "https://github.com/sponsors/isaacs" 1141 | } 1142 | }, 1143 | "node_modules/picocolors": { 1144 | "version": "1.0.1", 1145 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", 1146 | "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", 1147 | "dev": true 1148 | }, 1149 | "node_modules/picomatch": { 1150 | "version": "2.3.1", 1151 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1152 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1153 | "dev": true, 1154 | "engines": { 1155 | "node": ">=8.6" 1156 | }, 1157 | "funding": { 1158 | "url": "https://github.com/sponsors/jonschlinkert" 1159 | } 1160 | }, 1161 | "node_modules/pify": { 1162 | "version": "2.3.0", 1163 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1164 | "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", 1165 | "dev": true, 1166 | "engines": { 1167 | "node": ">=0.10.0" 1168 | } 1169 | }, 1170 | "node_modules/pirates": { 1171 | "version": "4.0.6", 1172 | "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", 1173 | "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", 1174 | "dev": true, 1175 | "engines": { 1176 | "node": ">= 6" 1177 | } 1178 | }, 1179 | "node_modules/postcss": { 1180 | "version": "8.4.38", 1181 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", 1182 | "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", 1183 | "dev": true, 1184 | "funding": [ 1185 | { 1186 | "type": "opencollective", 1187 | "url": "https://opencollective.com/postcss/" 1188 | }, 1189 | { 1190 | "type": "tidelift", 1191 | "url": "https://tidelift.com/funding/github/npm/postcss" 1192 | }, 1193 | { 1194 | "type": "github", 1195 | "url": "https://github.com/sponsors/ai" 1196 | } 1197 | ], 1198 | "dependencies": { 1199 | "nanoid": "^3.3.7", 1200 | "picocolors": "^1.0.0", 1201 | "source-map-js": "^1.2.0" 1202 | }, 1203 | "engines": { 1204 | "node": "^10 || ^12 || >=14" 1205 | } 1206 | }, 1207 | "node_modules/postcss-import": { 1208 | "version": "15.1.0", 1209 | "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", 1210 | "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", 1211 | "dev": true, 1212 | "dependencies": { 1213 | "postcss-value-parser": "^4.0.0", 1214 | "read-cache": "^1.0.0", 1215 | "resolve": "^1.1.7" 1216 | }, 1217 | "engines": { 1218 | "node": ">=14.0.0" 1219 | }, 1220 | "peerDependencies": { 1221 | "postcss": "^8.0.0" 1222 | } 1223 | }, 1224 | "node_modules/postcss-js": { 1225 | "version": "4.0.1", 1226 | "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", 1227 | "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", 1228 | "dev": true, 1229 | "dependencies": { 1230 | "camelcase-css": "^2.0.1" 1231 | }, 1232 | "engines": { 1233 | "node": "^12 || ^14 || >= 16" 1234 | }, 1235 | "funding": { 1236 | "type": "opencollective", 1237 | "url": "https://opencollective.com/postcss/" 1238 | }, 1239 | "peerDependencies": { 1240 | "postcss": "^8.4.21" 1241 | } 1242 | }, 1243 | "node_modules/postcss-load-config": { 1244 | "version": "4.0.2", 1245 | "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", 1246 | "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", 1247 | "dev": true, 1248 | "funding": [ 1249 | { 1250 | "type": "opencollective", 1251 | "url": "https://opencollective.com/postcss/" 1252 | }, 1253 | { 1254 | "type": "github", 1255 | "url": "https://github.com/sponsors/ai" 1256 | } 1257 | ], 1258 | "dependencies": { 1259 | "lilconfig": "^3.0.0", 1260 | "yaml": "^2.3.4" 1261 | }, 1262 | "engines": { 1263 | "node": ">= 14" 1264 | }, 1265 | "peerDependencies": { 1266 | "postcss": ">=8.0.9", 1267 | "ts-node": ">=9.0.0" 1268 | }, 1269 | "peerDependenciesMeta": { 1270 | "postcss": { 1271 | "optional": true 1272 | }, 1273 | "ts-node": { 1274 | "optional": true 1275 | } 1276 | } 1277 | }, 1278 | "node_modules/postcss-load-config/node_modules/lilconfig": { 1279 | "version": "3.1.1", 1280 | "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", 1281 | "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", 1282 | "dev": true, 1283 | "engines": { 1284 | "node": ">=14" 1285 | }, 1286 | "funding": { 1287 | "url": "https://github.com/sponsors/antonk52" 1288 | } 1289 | }, 1290 | "node_modules/postcss-nested": { 1291 | "version": "6.0.1", 1292 | "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", 1293 | "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", 1294 | "dev": true, 1295 | "dependencies": { 1296 | "postcss-selector-parser": "^6.0.11" 1297 | }, 1298 | "engines": { 1299 | "node": ">=12.0" 1300 | }, 1301 | "funding": { 1302 | "type": "opencollective", 1303 | "url": "https://opencollective.com/postcss/" 1304 | }, 1305 | "peerDependencies": { 1306 | "postcss": "^8.2.14" 1307 | } 1308 | }, 1309 | "node_modules/postcss-selector-parser": { 1310 | "version": "6.1.0", 1311 | "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", 1312 | "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", 1313 | "dev": true, 1314 | "dependencies": { 1315 | "cssesc": "^3.0.0", 1316 | "util-deprecate": "^1.0.2" 1317 | }, 1318 | "engines": { 1319 | "node": ">=4" 1320 | } 1321 | }, 1322 | "node_modules/postcss-value-parser": { 1323 | "version": "4.2.0", 1324 | "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 1325 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 1326 | "dev": true 1327 | }, 1328 | "node_modules/queue-microtask": { 1329 | "version": "1.2.3", 1330 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1331 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1332 | "dev": true, 1333 | "funding": [ 1334 | { 1335 | "type": "github", 1336 | "url": "https://github.com/sponsors/feross" 1337 | }, 1338 | { 1339 | "type": "patreon", 1340 | "url": "https://www.patreon.com/feross" 1341 | }, 1342 | { 1343 | "type": "consulting", 1344 | "url": "https://feross.org/support" 1345 | } 1346 | ] 1347 | }, 1348 | "node_modules/read-cache": { 1349 | "version": "1.0.0", 1350 | "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", 1351 | "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", 1352 | "dev": true, 1353 | "dependencies": { 1354 | "pify": "^2.3.0" 1355 | } 1356 | }, 1357 | "node_modules/readdirp": { 1358 | "version": "3.6.0", 1359 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1360 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1361 | "dev": true, 1362 | "dependencies": { 1363 | "picomatch": "^2.2.1" 1364 | }, 1365 | "engines": { 1366 | "node": ">=8.10.0" 1367 | } 1368 | }, 1369 | "node_modules/resolve": { 1370 | "version": "1.22.8", 1371 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 1372 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 1373 | "dev": true, 1374 | "dependencies": { 1375 | "is-core-module": "^2.13.0", 1376 | "path-parse": "^1.0.7", 1377 | "supports-preserve-symlinks-flag": "^1.0.0" 1378 | }, 1379 | "bin": { 1380 | "resolve": "bin/resolve" 1381 | }, 1382 | "funding": { 1383 | "url": "https://github.com/sponsors/ljharb" 1384 | } 1385 | }, 1386 | "node_modules/reusify": { 1387 | "version": "1.0.4", 1388 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1389 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1390 | "dev": true, 1391 | "engines": { 1392 | "iojs": ">=1.0.0", 1393 | "node": ">=0.10.0" 1394 | } 1395 | }, 1396 | "node_modules/run-parallel": { 1397 | "version": "1.2.0", 1398 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1399 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1400 | "dev": true, 1401 | "funding": [ 1402 | { 1403 | "type": "github", 1404 | "url": "https://github.com/sponsors/feross" 1405 | }, 1406 | { 1407 | "type": "patreon", 1408 | "url": "https://www.patreon.com/feross" 1409 | }, 1410 | { 1411 | "type": "consulting", 1412 | "url": "https://feross.org/support" 1413 | } 1414 | ], 1415 | "dependencies": { 1416 | "queue-microtask": "^1.2.2" 1417 | } 1418 | }, 1419 | "node_modules/shebang-command": { 1420 | "version": "2.0.0", 1421 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1422 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1423 | "dev": true, 1424 | "dependencies": { 1425 | "shebang-regex": "^3.0.0" 1426 | }, 1427 | "engines": { 1428 | "node": ">=8" 1429 | } 1430 | }, 1431 | "node_modules/shebang-regex": { 1432 | "version": "3.0.0", 1433 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1434 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1435 | "dev": true, 1436 | "engines": { 1437 | "node": ">=8" 1438 | } 1439 | }, 1440 | "node_modules/signal-exit": { 1441 | "version": "4.1.0", 1442 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 1443 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 1444 | "dev": true, 1445 | "engines": { 1446 | "node": ">=14" 1447 | }, 1448 | "funding": { 1449 | "url": "https://github.com/sponsors/isaacs" 1450 | } 1451 | }, 1452 | "node_modules/source-map-js": { 1453 | "version": "1.2.0", 1454 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", 1455 | "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", 1456 | "dev": true, 1457 | "engines": { 1458 | "node": ">=0.10.0" 1459 | } 1460 | }, 1461 | "node_modules/string-width": { 1462 | "version": "5.1.2", 1463 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 1464 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 1465 | "dev": true, 1466 | "dependencies": { 1467 | "eastasianwidth": "^0.2.0", 1468 | "emoji-regex": "^9.2.2", 1469 | "strip-ansi": "^7.0.1" 1470 | }, 1471 | "engines": { 1472 | "node": ">=12" 1473 | }, 1474 | "funding": { 1475 | "url": "https://github.com/sponsors/sindresorhus" 1476 | } 1477 | }, 1478 | "node_modules/string-width-cjs": { 1479 | "name": "string-width", 1480 | "version": "4.2.3", 1481 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1482 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1483 | "dev": true, 1484 | "dependencies": { 1485 | "emoji-regex": "^8.0.0", 1486 | "is-fullwidth-code-point": "^3.0.0", 1487 | "strip-ansi": "^6.0.1" 1488 | }, 1489 | "engines": { 1490 | "node": ">=8" 1491 | } 1492 | }, 1493 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 1494 | "version": "5.0.1", 1495 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1496 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1497 | "dev": true, 1498 | "engines": { 1499 | "node": ">=8" 1500 | } 1501 | }, 1502 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 1503 | "version": "8.0.0", 1504 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1505 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1506 | "dev": true 1507 | }, 1508 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 1509 | "version": "6.0.1", 1510 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1511 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1512 | "dev": true, 1513 | "dependencies": { 1514 | "ansi-regex": "^5.0.1" 1515 | }, 1516 | "engines": { 1517 | "node": ">=8" 1518 | } 1519 | }, 1520 | "node_modules/strip-ansi": { 1521 | "version": "7.1.0", 1522 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 1523 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 1524 | "dev": true, 1525 | "dependencies": { 1526 | "ansi-regex": "^6.0.1" 1527 | }, 1528 | "engines": { 1529 | "node": ">=12" 1530 | }, 1531 | "funding": { 1532 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 1533 | } 1534 | }, 1535 | "node_modules/strip-ansi-cjs": { 1536 | "name": "strip-ansi", 1537 | "version": "6.0.1", 1538 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1539 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1540 | "dev": true, 1541 | "dependencies": { 1542 | "ansi-regex": "^5.0.1" 1543 | }, 1544 | "engines": { 1545 | "node": ">=8" 1546 | } 1547 | }, 1548 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 1549 | "version": "5.0.1", 1550 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1551 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1552 | "dev": true, 1553 | "engines": { 1554 | "node": ">=8" 1555 | } 1556 | }, 1557 | "node_modules/sucrase": { 1558 | "version": "3.35.0", 1559 | "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", 1560 | "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", 1561 | "dev": true, 1562 | "dependencies": { 1563 | "@jridgewell/gen-mapping": "^0.3.2", 1564 | "commander": "^4.0.0", 1565 | "glob": "^10.3.10", 1566 | "lines-and-columns": "^1.1.6", 1567 | "mz": "^2.7.0", 1568 | "pirates": "^4.0.1", 1569 | "ts-interface-checker": "^0.1.9" 1570 | }, 1571 | "bin": { 1572 | "sucrase": "bin/sucrase", 1573 | "sucrase-node": "bin/sucrase-node" 1574 | }, 1575 | "engines": { 1576 | "node": ">=16 || 14 >=14.17" 1577 | } 1578 | }, 1579 | "node_modules/supports-preserve-symlinks-flag": { 1580 | "version": "1.0.0", 1581 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1582 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1583 | "dev": true, 1584 | "engines": { 1585 | "node": ">= 0.4" 1586 | }, 1587 | "funding": { 1588 | "url": "https://github.com/sponsors/ljharb" 1589 | } 1590 | }, 1591 | "node_modules/tailwindcss": { 1592 | "version": "3.4.3", 1593 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", 1594 | "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", 1595 | "dev": true, 1596 | "dependencies": { 1597 | "@alloc/quick-lru": "^5.2.0", 1598 | "arg": "^5.0.2", 1599 | "chokidar": "^3.5.3", 1600 | "didyoumean": "^1.2.2", 1601 | "dlv": "^1.1.3", 1602 | "fast-glob": "^3.3.0", 1603 | "glob-parent": "^6.0.2", 1604 | "is-glob": "^4.0.3", 1605 | "jiti": "^1.21.0", 1606 | "lilconfig": "^2.1.0", 1607 | "micromatch": "^4.0.5", 1608 | "normalize-path": "^3.0.0", 1609 | "object-hash": "^3.0.0", 1610 | "picocolors": "^1.0.0", 1611 | "postcss": "^8.4.23", 1612 | "postcss-import": "^15.1.0", 1613 | "postcss-js": "^4.0.1", 1614 | "postcss-load-config": "^4.0.1", 1615 | "postcss-nested": "^6.0.1", 1616 | "postcss-selector-parser": "^6.0.11", 1617 | "resolve": "^1.22.2", 1618 | "sucrase": "^3.32.0" 1619 | }, 1620 | "bin": { 1621 | "tailwind": "lib/cli.js", 1622 | "tailwindcss": "lib/cli.js" 1623 | }, 1624 | "engines": { 1625 | "node": ">=14.0.0" 1626 | } 1627 | }, 1628 | "node_modules/thenify": { 1629 | "version": "3.3.1", 1630 | "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", 1631 | "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", 1632 | "dev": true, 1633 | "dependencies": { 1634 | "any-promise": "^1.0.0" 1635 | } 1636 | }, 1637 | "node_modules/thenify-all": { 1638 | "version": "1.6.0", 1639 | "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 1640 | "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", 1641 | "dev": true, 1642 | "dependencies": { 1643 | "thenify": ">= 3.1.0 < 4" 1644 | }, 1645 | "engines": { 1646 | "node": ">=0.8" 1647 | } 1648 | }, 1649 | "node_modules/to-regex-range": { 1650 | "version": "5.0.1", 1651 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1652 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1653 | "dev": true, 1654 | "dependencies": { 1655 | "is-number": "^7.0.0" 1656 | }, 1657 | "engines": { 1658 | "node": ">=8.0" 1659 | } 1660 | }, 1661 | "node_modules/ts-interface-checker": { 1662 | "version": "0.1.13", 1663 | "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", 1664 | "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", 1665 | "dev": true 1666 | }, 1667 | "node_modules/util-deprecate": { 1668 | "version": "1.0.2", 1669 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1670 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 1671 | "dev": true 1672 | }, 1673 | "node_modules/which": { 1674 | "version": "2.0.2", 1675 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1676 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1677 | "dev": true, 1678 | "dependencies": { 1679 | "isexe": "^2.0.0" 1680 | }, 1681 | "bin": { 1682 | "node-which": "bin/node-which" 1683 | }, 1684 | "engines": { 1685 | "node": ">= 8" 1686 | } 1687 | }, 1688 | "node_modules/wrap-ansi": { 1689 | "version": "8.1.0", 1690 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 1691 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 1692 | "dev": true, 1693 | "dependencies": { 1694 | "ansi-styles": "^6.1.0", 1695 | "string-width": "^5.0.1", 1696 | "strip-ansi": "^7.0.1" 1697 | }, 1698 | "engines": { 1699 | "node": ">=12" 1700 | }, 1701 | "funding": { 1702 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1703 | } 1704 | }, 1705 | "node_modules/wrap-ansi-cjs": { 1706 | "name": "wrap-ansi", 1707 | "version": "7.0.0", 1708 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1709 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1710 | "dev": true, 1711 | "dependencies": { 1712 | "ansi-styles": "^4.0.0", 1713 | "string-width": "^4.1.0", 1714 | "strip-ansi": "^6.0.0" 1715 | }, 1716 | "engines": { 1717 | "node": ">=10" 1718 | }, 1719 | "funding": { 1720 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1721 | } 1722 | }, 1723 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 1724 | "version": "5.0.1", 1725 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1726 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1727 | "dev": true, 1728 | "engines": { 1729 | "node": ">=8" 1730 | } 1731 | }, 1732 | "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { 1733 | "version": "4.3.0", 1734 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1735 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1736 | "dev": true, 1737 | "dependencies": { 1738 | "color-convert": "^2.0.1" 1739 | }, 1740 | "engines": { 1741 | "node": ">=8" 1742 | }, 1743 | "funding": { 1744 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1745 | } 1746 | }, 1747 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 1748 | "version": "8.0.0", 1749 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1750 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1751 | "dev": true 1752 | }, 1753 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 1754 | "version": "4.2.3", 1755 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1756 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1757 | "dev": true, 1758 | "dependencies": { 1759 | "emoji-regex": "^8.0.0", 1760 | "is-fullwidth-code-point": "^3.0.0", 1761 | "strip-ansi": "^6.0.1" 1762 | }, 1763 | "engines": { 1764 | "node": ">=8" 1765 | } 1766 | }, 1767 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 1768 | "version": "6.0.1", 1769 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1770 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1771 | "dev": true, 1772 | "dependencies": { 1773 | "ansi-regex": "^5.0.1" 1774 | }, 1775 | "engines": { 1776 | "node": ">=8" 1777 | } 1778 | }, 1779 | "node_modules/yaml": { 1780 | "version": "2.4.3", 1781 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", 1782 | "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", 1783 | "dev": true, 1784 | "bin": { 1785 | "yaml": "bin.mjs" 1786 | }, 1787 | "engines": { 1788 | "node": ">= 14" 1789 | } 1790 | } 1791 | } 1792 | } 1793 | -------------------------------------------------------------------------------- /bootstrap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superkit", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "tailwindcss": "^3.4.3", 13 | "esbuild": "^0.21.5" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bootstrap/pkg/.keep: -------------------------------------------------------------------------------- 1 | Add your packages aka custom libraries here. 2 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/auth_handler.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "AABBCCDD/app/db" 5 | "database/sql" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/anthdm/superkit/kit" 12 | v "github.com/anthdm/superkit/validate" 13 | "github.com/golang-jwt/jwt/v5" 14 | "github.com/google/uuid" 15 | "golang.org/x/crypto/bcrypt" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | const ( 20 | userSessionName = "user-session" 21 | ) 22 | 23 | var authSchema = v.Schema{ 24 | "email": v.Rules(v.Email), 25 | "password": v.Rules(v.Required), 26 | } 27 | 28 | func HandleLoginIndex(kit *kit.Kit) error { 29 | if kit.Auth().Check() { 30 | redirectURL := kit.Getenv("SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN", "/profile") 31 | return kit.Redirect(http.StatusSeeOther, redirectURL) 32 | } 33 | return kit.Render(LoginIndex(LoginIndexPageData{})) 34 | } 35 | 36 | func HandleLoginCreate(kit *kit.Kit) error { 37 | var values LoginFormValues 38 | errors, ok := v.Request(kit.Request, &values, authSchema) 39 | if !ok { 40 | return kit.Render(LoginForm(values, errors)) 41 | } 42 | 43 | var user User 44 | err := db.Get().Find(&user, "email = ?", values.Email).Error 45 | if err != nil { 46 | if err == gorm.ErrRecordNotFound { 47 | errors.Add("credentials", "invalid credentials") 48 | return kit.Render(LoginForm(values, errors)) 49 | } 50 | } 51 | 52 | err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(values.Password)) 53 | if err != nil { 54 | errors.Add("credentials", "invalid credentials") 55 | return kit.Render(LoginForm(values, errors)) 56 | } 57 | 58 | skipVerify := kit.Getenv("SUPERKIT_AUTH_SKIP_VERIFY", "false") 59 | if skipVerify != "true" { 60 | if !user.EmailVerifiedAt.Valid { 61 | errors.Add("verified", "please verify your email") 62 | return kit.Render(LoginForm(values, errors)) 63 | } 64 | } 65 | 66 | sessionExpiryStr := kit.Getenv("SUPERKIT_AUTH_SESSION_EXPIRY_IN_HOURS", "48") 67 | sessionExpiry, err := strconv.Atoi(sessionExpiryStr) 68 | if err != nil { 69 | sessionExpiry = 48 70 | } 71 | session := Session{ 72 | UserID: user.ID, 73 | Token: uuid.New().String(), 74 | ExpiresAt: time.Now().Add(time.Hour * time.Duration(sessionExpiry)), 75 | } 76 | if err = db.Get().Create(&session).Error; err != nil { 77 | return err 78 | } 79 | 80 | sess := kit.GetSession(userSessionName) 81 | sess.Values["sessionToken"] = session.Token 82 | sess.Save(kit.Request, kit.Response) 83 | redirectURL := kit.Getenv("SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN", "/profile") 84 | 85 | return kit.Redirect(http.StatusSeeOther, redirectURL) 86 | } 87 | 88 | func HandleLoginDelete(kit *kit.Kit) error { 89 | sess := kit.GetSession(userSessionName) 90 | defer func() { 91 | sess.Values = map[any]any{} 92 | sess.Save(kit.Request, kit.Response) 93 | }() 94 | err := db.Get().Delete(&Session{}, "token = ?", sess.Values["sessionToken"]).Error 95 | if err != nil { 96 | return err 97 | } 98 | return kit.Redirect(http.StatusSeeOther, "/") 99 | } 100 | 101 | func HandleEmailVerify(kit *kit.Kit) error { 102 | tokenStr := kit.Request.URL.Query().Get("token") 103 | if len(tokenStr) == 0 { 104 | return kit.Render(EmailVerificationError("invalid verification token")) 105 | } 106 | 107 | token, err := jwt.ParseWithClaims( 108 | tokenStr, &jwt.RegisteredClaims{}, func(token *jwt.Token) (any, error) { 109 | return []byte(os.Getenv("SUPERKIT_SECRET")), nil 110 | }, jwt.WithLeeway(5*time.Second)) 111 | if err != nil { 112 | return kit.Render(EmailVerificationError("invalid verification token")) 113 | } 114 | if !token.Valid { 115 | return kit.Render(EmailVerificationError("invalid verification token")) 116 | } 117 | 118 | claims, ok := token.Claims.(*jwt.RegisteredClaims) 119 | if !ok { 120 | return kit.Render(EmailVerificationError("invalid verification token")) 121 | } 122 | if claims.ExpiresAt.Time.Before(time.Now()) { 123 | return kit.Render(EmailVerificationError("Email verification token expired")) 124 | } 125 | 126 | userID, err := strconv.Atoi(claims.Subject) 127 | if err != nil { 128 | return kit.Render(EmailVerificationError("Email verification token expired")) 129 | } 130 | 131 | var user User 132 | err = db.Get().First(&user, userID).Error 133 | if err != nil { 134 | return err 135 | } 136 | 137 | if user.EmailVerifiedAt.Time.After(time.Time{}) { 138 | return kit.Render(EmailVerificationError("Email already verified")) 139 | } 140 | 141 | now := sql.NullTime{Time: time.Now(), Valid: true} 142 | user.EmailVerifiedAt = now 143 | err = db.Get().Save(&user).Error 144 | if err != nil { 145 | return err 146 | } 147 | 148 | return kit.Redirect(http.StatusSeeOther, "/login") 149 | } 150 | 151 | func AuthenticateUser(kit *kit.Kit) (kit.Auth, error) { 152 | auth := Auth{} 153 | sess := kit.GetSession(userSessionName) 154 | token, ok := sess.Values["sessionToken"] 155 | if !ok { 156 | return auth, nil 157 | } 158 | 159 | var session Session 160 | err := db.Get(). 161 | Preload("User"). 162 | Find(&session, "token = ? AND expires_at > ?", token, time.Now()).Error 163 | if err != nil || session.ID == 0 { 164 | return auth, nil 165 | } 166 | 167 | return Auth{ 168 | LoggedIn: true, 169 | UserID: session.User.ID, 170 | Email: session.User.Email, 171 | }, nil 172 | } 173 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/email_verification_error_view.templ: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "AABBCCDD/app/views/layouts" 5 | ) 6 | 7 | templ EmailVerificationError(errorMessage string) { 8 | @layouts.BaseLayout() { 9 |
10 |
{ errorMessage }
11 | back to homepage 12 |
13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/login_view.templ: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | v "github.com/anthdm/superkit/validate" 5 | 6 | "AABBCCDD/app/views/layouts" 7 | "AABBCCDD/app/views/components" 8 | ) 9 | 10 | type LoginIndexPageData struct { 11 | FormValues LoginFormValues 12 | FormErrors v.Errors 13 | } 14 | 15 | type LoginFormValues struct { 16 | Email string `form:"email"` 17 | Password string `form:"password"` 18 | } 19 | 20 | templ LoginIndex(data LoginIndexPageData) { 21 | @layouts.BaseLayout() { 22 |
23 | @components.ThemeSwitcher() 24 |
25 |
26 |
27 |
28 |

Login to SuperKit

29 | @LoginForm(data.FormValues, data.FormErrors) 30 | Don't have an account? Signup here. 31 |
32 |
33 |
34 | } 35 | } 36 | 37 | templ LoginForm(values LoginFormValues, errors v.Errors) { 38 |
39 |
40 | 41 | 42 | if errors.Has("email") { 43 |
{ errors.Get("email")[0] }
44 | } 45 |
46 |
47 | 48 | 49 | if errors.Has("password") { 50 | 55 | } 56 | if errors.Has("credentials") { 57 |
{ errors.Get("credentials")[0] }
58 | } 59 | if errors.Has("verified") { 60 |
{ errors.Get("verified")[0] }
61 | } 62 |
63 | 66 |
67 | } 68 | 69 | func buttonAttrs() templ.Attributes { 70 | class := "inline-flex text-primary-foreground items-center justify-center px-4 py-2 font-medium text-sm tracking-wide transition-colors duration-200 rounded-md bg-primary text-foreground hover:bg-primary/90 focus:ring focus:ring-primary focus:shadow-outline focus:outline-none" 71 | return templ.Attributes{ 72 | "class": class, 73 | } 74 | } 75 | 76 | func inputAttrs(hasError bool) templ.Attributes { 77 | class := "flex w-full px-3 py-2 bg-transparent text-sm border rounded-md ring-offset-background placeholder:text-neutral-500 focus:border-neutral-300 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50" 78 | if hasError { 79 | class += " border-red-500" 80 | } else { 81 | class += " border-input" 82 | } 83 | return templ.Attributes{ 84 | "class": class, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/profile_handler.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "AABBCCDD/app/db" 5 | "fmt" 6 | 7 | "github.com/anthdm/superkit/kit" 8 | v "github.com/anthdm/superkit/validate" 9 | ) 10 | 11 | var profileSchema = v.Schema{ 12 | "firstName": v.Rules(v.Min(3), v.Max(50)), 13 | "lastName": v.Rules(v.Min(3), v.Max(50)), 14 | } 15 | 16 | type ProfileFormValues struct { 17 | ID uint `form:"id"` 18 | FirstName string `form:"firstName"` 19 | LastName string `form:"lastName"` 20 | Email string 21 | Success string 22 | } 23 | 24 | func HandleProfileShow(kit *kit.Kit) error { 25 | auth := kit.Auth().(Auth) 26 | 27 | var user User 28 | if err := db.Get().First(&user, auth.UserID).Error; err != nil { 29 | return err 30 | } 31 | 32 | formValues := ProfileFormValues{ 33 | ID: user.ID, 34 | FirstName: user.FirstName, 35 | LastName: user.LastName, 36 | Email: user.Email, 37 | } 38 | 39 | return kit.Render(ProfileShow(formValues)) 40 | } 41 | 42 | func HandleProfileUpdate(kit *kit.Kit) error { 43 | var values ProfileFormValues 44 | errors, ok := v.Request(kit.Request, &values, profileSchema) 45 | if !ok { 46 | return kit.Render(ProfileForm(values, errors)) 47 | } 48 | 49 | auth := kit.Auth().(Auth) 50 | if auth.UserID != values.ID { 51 | return fmt.Errorf("unauthorized request for profile %d", values.ID) 52 | } 53 | err := db.Get().Model(&User{}). 54 | Where("id = ?", auth.UserID). 55 | Updates(&User{ 56 | FirstName: values.FirstName, 57 | LastName: values.LastName, 58 | }).Error 59 | if err != nil { 60 | return err 61 | } 62 | 63 | values.Success = "Profile successfully updated!" 64 | values.Email = auth.Email 65 | 66 | return kit.Render(ProfileForm(values, v.Errors{})) 67 | } 68 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/profile_view.templ: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | 6 | v "github.com/anthdm/superkit/validate" 7 | 8 | "AABBCCDD/app/views/layouts" 9 | ) 10 | 11 | templ ProfileShow(formValues ProfileFormValues) { 12 | @layouts.App() { 13 |
14 |
15 |

Welcome, { formValues.FirstName } { formValues.LastName }

16 |
17 | back to home 18 | 19 |
20 |
21 | @ProfileForm(formValues, v.Errors{}) 22 |
23 | } 24 | } 25 | 26 | templ ProfileForm(values ProfileFormValues, errors v.Errors) { 27 |
28 | 29 |
30 | 31 | 32 | if errors.Has("firstName") { 33 |
{ errors.Get("firstName")[0] }
34 | } 35 |
36 |
37 | 38 | 39 | if errors.Has("lastName") { 40 |
{ errors.Get("lastName")[0] }
41 | } 42 |
43 |
44 | 45 |
{ values.Email }
46 |
47 | 48 | if len(values.Success) > 0 { 49 |
{ values.Success }
50 | } 51 |
52 | } 53 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/routes.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/anthdm/superkit/kit" 5 | "github.com/go-chi/chi/v5" 6 | ) 7 | 8 | func InitializeRoutes(router chi.Router) { 9 | authConfig := kit.AuthenticationConfig{ 10 | AuthFunc: AuthenticateUser, 11 | RedirectURL: "/login", 12 | } 13 | 14 | router.Get("/email/verify", kit.Handler(HandleEmailVerify)) 15 | router.Post("/resend-email-verification", kit.Handler(HandleResendVerificationCode)) 16 | 17 | router.Group(func(auth chi.Router) { 18 | auth.Use(kit.WithAuthentication(authConfig, false)) 19 | auth.Get("/login", kit.Handler(HandleLoginIndex)) 20 | auth.Post("/login", kit.Handler(HandleLoginCreate)) 21 | auth.Delete("/logout", kit.Handler(HandleLoginDelete)) 22 | 23 | auth.Get("/signup", kit.Handler(HandleSignupIndex)) 24 | auth.Post("/signup", kit.Handler(HandleSignupCreate)) 25 | 26 | }) 27 | 28 | router.Group(func(auth chi.Router) { 29 | auth.Use(kit.WithAuthentication(authConfig, true)) 30 | auth.Get("/profile", kit.Handler(HandleProfileShow)) 31 | auth.Put("/profile", kit.Handler(HandleProfileUpdate)) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/signup_handler.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "AABBCCDD/app/db" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/anthdm/superkit/event" 12 | "github.com/anthdm/superkit/kit" 13 | v "github.com/anthdm/superkit/validate" 14 | "github.com/golang-jwt/jwt/v5" 15 | ) 16 | 17 | var signupSchema = v.Schema{ 18 | "email": v.Rules(v.Email), 19 | "password": v.Rules( 20 | v.ContainsSpecial, 21 | v.ContainsUpper, 22 | v.Min(7), 23 | v.Max(50), 24 | ), 25 | "firstName": v.Rules(v.Min(2), v.Max(50)), 26 | "lastName": v.Rules(v.Min(2), v.Max(50)), 27 | } 28 | 29 | func HandleSignupIndex(kit *kit.Kit) error { 30 | return kit.Render(SignupIndex(SignupIndexPageData{})) 31 | } 32 | 33 | func HandleSignupCreate(kit *kit.Kit) error { 34 | var values SignupFormValues 35 | errors, ok := v.Request(kit.Request, &values, signupSchema) 36 | if !ok { 37 | return kit.Render(SignupForm(values, errors)) 38 | } 39 | if values.Password != values.PasswordConfirm { 40 | errors.Add("passwordConfirm", "passwords do not match") 41 | return kit.Render(SignupForm(values, errors)) 42 | } 43 | user, err := createUserFromFormValues(values) 44 | if err != nil { 45 | return err 46 | } 47 | token, err := createVerificationToken(user.ID) 48 | if err != nil { 49 | return err 50 | } 51 | event.Emit(UserSignupEvent, UserWithVerificationToken{ 52 | Token: token, 53 | User: user, 54 | }) 55 | return kit.Render(ConfirmEmail(user)) 56 | } 57 | 58 | func HandleResendVerificationCode(kit *kit.Kit) error { 59 | idstr := kit.FormValue("userID") 60 | id, err := strconv.Atoi(idstr) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | var user User 66 | if err = db.Get().First(&user, id).Error; err != nil { 67 | return kit.Text(http.StatusOK, "An unexpected error occured") 68 | } 69 | 70 | if user.EmailVerifiedAt.Time.After(time.Time{}) { 71 | return kit.Text(http.StatusOK, "Email already verified!") 72 | } 73 | 74 | token, err := createVerificationToken(uint(id)) 75 | if err != nil { 76 | return kit.Text(http.StatusOK, "An unexpected error occured") 77 | } 78 | 79 | event.Emit(ResendVerificationEvent, UserWithVerificationToken{ 80 | User: user, 81 | Token: token, 82 | }) 83 | 84 | msg := fmt.Sprintf("A new verification token has been sent to %s", user.Email) 85 | 86 | return kit.Text(http.StatusOK, msg) 87 | } 88 | 89 | func createVerificationToken(userID uint) (string, error) { 90 | expiryStr := kit.Getenv("SUPERKIT_AUTH_EMAIL_VERIFICATION_EXPIRY_IN_HOURS", "1") 91 | expiry, err := strconv.Atoi(expiryStr) 92 | if err != nil { 93 | expiry = 1 94 | } 95 | 96 | claims := jwt.RegisteredClaims{ 97 | Subject: fmt.Sprint(userID), 98 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(expiry))), 99 | } 100 | 101 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 102 | 103 | return token.SignedString([]byte(os.Getenv("SUPERKIT_SECRET"))) 104 | } 105 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/signup_view.templ: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | v "github.com/anthdm/superkit/validate" 5 | "AABBCCDD/app/views/layouts" 6 | "AABBCCDD/app/views/components" 7 | 8 | "fmt" 9 | ) 10 | 11 | type SignupIndexPageData struct { 12 | FormValues SignupFormValues 13 | FormErrors v.Errors 14 | } 15 | 16 | type SignupFormValues struct { 17 | Email string `form:"email"` 18 | FirstName string `form:"firstName"` 19 | LastName string `form:"lastName"` 20 | Password string `form:"password"` 21 | PasswordConfirm string `form:"passwordConfirm"` 22 | } 23 | 24 | templ SignupIndex(data SignupIndexPageData) { 25 | @layouts.BaseLayout() { 26 |
27 | @components.ThemeSwitcher() 28 |
29 |
30 |
31 |
32 |

Signup

33 | @SignupForm(data.FormValues, data.FormErrors) 34 |
35 |
36 |
37 | } 38 | } 39 | 40 | templ SignupForm(values SignupFormValues, errors v.Errors) { 41 |
42 |
43 | 44 | 45 | if errors.Has("email") { 46 |
{ errors.Get("email")[0] }
47 | } 48 |
49 |
50 | 51 | 52 | if errors.Has("fistName") { 53 | 58 | } 59 |
60 |
61 | 62 | 63 | if errors.Has("lastName") { 64 | 69 | } 70 |
71 |
72 | 73 | 74 | if errors.Has("password") { 75 | 80 | } 81 |
82 |
83 | 84 | 85 | if errors.Has("passwordConfirm") { 86 |
{ errors.Get("passwordConfirm")[0] }
87 | } 88 |
89 | 92 | Already have an account? Login here. 93 |
94 | } 95 | 96 | templ ConfirmEmail(user User) { 97 |
98 | 99 |
An email confirmation link has been sent to: { user.Email }
100 |
Trouble receiving the verification code?
101 |
102 | } 103 | -------------------------------------------------------------------------------- /bootstrap/plugins/auth/types.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "AABBCCDD/app/db" 5 | "database/sql" 6 | "time" 7 | 8 | "golang.org/x/crypto/bcrypt" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | // Event name constants 13 | const ( 14 | UserSignupEvent = "auth.signup" 15 | ResendVerificationEvent = "auth.resend.verification" 16 | ) 17 | 18 | // UserWithVerificationToken is a struct that will be sent over the 19 | // auth.signup event. It holds the User struct and the Verification token string. 20 | type UserWithVerificationToken struct { 21 | User User 22 | Token string 23 | } 24 | 25 | type Auth struct { 26 | UserID uint 27 | Email string 28 | LoggedIn bool 29 | } 30 | 31 | func (auth Auth) Check() bool { 32 | return auth.LoggedIn 33 | } 34 | 35 | type User struct { 36 | gorm.Model 37 | 38 | Email string 39 | FirstName string 40 | LastName string 41 | PasswordHash string 42 | EmailVerifiedAt sql.NullTime 43 | CreatedAt time.Time 44 | UpdatedAt time.Time 45 | } 46 | 47 | func createUserFromFormValues(values SignupFormValues) (User, error) { 48 | hash, err := bcrypt.GenerateFromPassword([]byte(values.Password), bcrypt.DefaultCost) 49 | if err != nil { 50 | return User{}, err 51 | } 52 | user := User{ 53 | Email: values.Email, 54 | FirstName: values.FirstName, 55 | LastName: values.LastName, 56 | PasswordHash: string(hash), 57 | } 58 | result := db.Get().Create(&user) 59 | return user, result.Error 60 | } 61 | 62 | type Session struct { 63 | gorm.Model 64 | 65 | UserID uint 66 | Token string 67 | IPAddress string 68 | UserAgent string 69 | ExpiresAt time.Time 70 | CreatedAt time.Time 71 | User User 72 | } 73 | -------------------------------------------------------------------------------- /bootstrap/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthdm/superkit/e7f8e0aad3e935ac5fb9824aee79ad4f377eee2a/bootstrap/public/assets/favicon.ico -------------------------------------------------------------------------------- /bootstrap/public/assets/index.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | // app/assets/index.js 3 | console.log("if you like superkit consider given it a star on GitHub."); 4 | })(); 5 | -------------------------------------------------------------------------------- /bootstrap/public/assets/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | 7. Disable tap highlights on iOS 36 | */ 37 | 38 | html, 39 | :host { 40 | line-height: 1.5; 41 | /* 1 */ 42 | -webkit-text-size-adjust: 100%; 43 | /* 2 */ 44 | -moz-tab-size: 4; 45 | /* 3 */ 46 | -o-tab-size: 4; 47 | tab-size: 4; 48 | /* 3 */ 49 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 50 | /* 4 */ 51 | font-feature-settings: normal; 52 | /* 5 */ 53 | font-variation-settings: normal; 54 | /* 6 */ 55 | -webkit-tap-highlight-color: transparent; 56 | /* 7 */ 57 | } 58 | 59 | /* 60 | 1. Remove the margin in all browsers. 61 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 62 | */ 63 | 64 | body { 65 | margin: 0; 66 | /* 1 */ 67 | line-height: inherit; 68 | /* 2 */ 69 | } 70 | 71 | /* 72 | 1. Add the correct height in Firefox. 73 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 74 | 3. Ensure horizontal rules are visible by default. 75 | */ 76 | 77 | hr { 78 | height: 0; 79 | /* 1 */ 80 | color: inherit; 81 | /* 2 */ 82 | border-top-width: 1px; 83 | /* 3 */ 84 | } 85 | 86 | /* 87 | Add the correct text decoration in Chrome, Edge, and Safari. 88 | */ 89 | 90 | abbr:where([title]) { 91 | -webkit-text-decoration: underline dotted; 92 | text-decoration: underline dotted; 93 | } 94 | 95 | /* 96 | Remove the default font size and weight for headings. 97 | */ 98 | 99 | h1, 100 | h2, 101 | h3, 102 | h4, 103 | h5, 104 | h6 { 105 | font-size: inherit; 106 | font-weight: inherit; 107 | } 108 | 109 | /* 110 | Reset links to optimize for opt-in styling instead of opt-out. 111 | */ 112 | 113 | a { 114 | color: inherit; 115 | text-decoration: inherit; 116 | } 117 | 118 | /* 119 | Add the correct font weight in Edge and Safari. 120 | */ 121 | 122 | b, 123 | strong { 124 | font-weight: bolder; 125 | } 126 | 127 | /* 128 | 1. Use the user's configured `mono` font-family by default. 129 | 2. Use the user's configured `mono` font-feature-settings by default. 130 | 3. Use the user's configured `mono` font-variation-settings by default. 131 | 4. Correct the odd `em` font sizing in all browsers. 132 | */ 133 | 134 | code, 135 | kbd, 136 | samp, 137 | pre { 138 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 139 | /* 1 */ 140 | font-feature-settings: normal; 141 | /* 2 */ 142 | font-variation-settings: normal; 143 | /* 3 */ 144 | font-size: 1em; 145 | /* 4 */ 146 | } 147 | 148 | /* 149 | Add the correct font size in all browsers. 150 | */ 151 | 152 | small { 153 | font-size: 80%; 154 | } 155 | 156 | /* 157 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 158 | */ 159 | 160 | sub, 161 | sup { 162 | font-size: 75%; 163 | line-height: 0; 164 | position: relative; 165 | vertical-align: baseline; 166 | } 167 | 168 | sub { 169 | bottom: -0.25em; 170 | } 171 | 172 | sup { 173 | top: -0.5em; 174 | } 175 | 176 | /* 177 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 178 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 179 | 3. Remove gaps between table borders by default. 180 | */ 181 | 182 | table { 183 | text-indent: 0; 184 | /* 1 */ 185 | border-color: inherit; 186 | /* 2 */ 187 | border-collapse: collapse; 188 | /* 3 */ 189 | } 190 | 191 | /* 192 | 1. Change the font styles in all browsers. 193 | 2. Remove the margin in Firefox and Safari. 194 | 3. Remove default padding in all browsers. 195 | */ 196 | 197 | button, 198 | input, 199 | optgroup, 200 | select, 201 | textarea { 202 | font-family: inherit; 203 | /* 1 */ 204 | font-feature-settings: inherit; 205 | /* 1 */ 206 | font-variation-settings: inherit; 207 | /* 1 */ 208 | font-size: 100%; 209 | /* 1 */ 210 | font-weight: inherit; 211 | /* 1 */ 212 | line-height: inherit; 213 | /* 1 */ 214 | letter-spacing: inherit; 215 | /* 1 */ 216 | color: inherit; 217 | /* 1 */ 218 | margin: 0; 219 | /* 2 */ 220 | padding: 0; 221 | /* 3 */ 222 | } 223 | 224 | /* 225 | Remove the inheritance of text transform in Edge and Firefox. 226 | */ 227 | 228 | button, 229 | select { 230 | text-transform: none; 231 | } 232 | 233 | /* 234 | 1. Correct the inability to style clickable types in iOS and Safari. 235 | 2. Remove default button styles. 236 | */ 237 | 238 | button, 239 | input:where([type='button']), 240 | input:where([type='reset']), 241 | input:where([type='submit']) { 242 | -webkit-appearance: button; 243 | /* 1 */ 244 | background-color: transparent; 245 | /* 2 */ 246 | background-image: none; 247 | /* 2 */ 248 | } 249 | 250 | /* 251 | Use the modern Firefox focus style for all focusable elements. 252 | */ 253 | 254 | :-moz-focusring { 255 | outline: auto; 256 | } 257 | 258 | /* 259 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 260 | */ 261 | 262 | :-moz-ui-invalid { 263 | box-shadow: none; 264 | } 265 | 266 | /* 267 | Add the correct vertical alignment in Chrome and Firefox. 268 | */ 269 | 270 | progress { 271 | vertical-align: baseline; 272 | } 273 | 274 | /* 275 | Correct the cursor style of increment and decrement buttons in Safari. 276 | */ 277 | 278 | ::-webkit-inner-spin-button, 279 | ::-webkit-outer-spin-button { 280 | height: auto; 281 | } 282 | 283 | /* 284 | 1. Correct the odd appearance in Chrome and Safari. 285 | 2. Correct the outline style in Safari. 286 | */ 287 | 288 | [type='search'] { 289 | -webkit-appearance: textfield; 290 | /* 1 */ 291 | outline-offset: -2px; 292 | /* 2 */ 293 | } 294 | 295 | /* 296 | Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | ::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /* 304 | 1. Correct the inability to style clickable types in iOS and Safari. 305 | 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; 310 | /* 1 */ 311 | font: inherit; 312 | /* 2 */ 313 | } 314 | 315 | /* 316 | Add the correct display in Chrome and Safari. 317 | */ 318 | 319 | summary { 320 | display: list-item; 321 | } 322 | 323 | /* 324 | Removes the default spacing and border for appropriate elements. 325 | */ 326 | 327 | blockquote, 328 | dl, 329 | dd, 330 | h1, 331 | h2, 332 | h3, 333 | h4, 334 | h5, 335 | h6, 336 | hr, 337 | figure, 338 | p, 339 | pre { 340 | margin: 0; 341 | } 342 | 343 | fieldset { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | legend { 349 | padding: 0; 350 | } 351 | 352 | ol, 353 | ul, 354 | menu { 355 | list-style: none; 356 | margin: 0; 357 | padding: 0; 358 | } 359 | 360 | /* 361 | Reset default styling for dialogs. 362 | */ 363 | 364 | dialog { 365 | padding: 0; 366 | } 367 | 368 | /* 369 | Prevent resizing textareas horizontally by default. 370 | */ 371 | 372 | textarea { 373 | resize: vertical; 374 | } 375 | 376 | /* 377 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 378 | 2. Set the default placeholder color to the user's configured gray 400 color. 379 | */ 380 | 381 | input::-moz-placeholder, textarea::-moz-placeholder { 382 | opacity: 1; 383 | /* 1 */ 384 | color: #9ca3af; 385 | /* 2 */ 386 | } 387 | 388 | input::placeholder, 389 | textarea::placeholder { 390 | opacity: 1; 391 | /* 1 */ 392 | color: #9ca3af; 393 | /* 2 */ 394 | } 395 | 396 | /* 397 | Set the default cursor for buttons. 398 | */ 399 | 400 | button, 401 | [role="button"] { 402 | cursor: pointer; 403 | } 404 | 405 | /* 406 | Make sure disabled buttons don't get the pointer cursor. 407 | */ 408 | 409 | :disabled { 410 | cursor: default; 411 | } 412 | 413 | /* 414 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 415 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 416 | This can trigger a poorly considered lint error in some tools but is included by design. 417 | */ 418 | 419 | img, 420 | svg, 421 | video, 422 | canvas, 423 | audio, 424 | iframe, 425 | embed, 426 | object { 427 | display: block; 428 | /* 1 */ 429 | vertical-align: middle; 430 | /* 2 */ 431 | } 432 | 433 | /* 434 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 435 | */ 436 | 437 | img, 438 | video { 439 | max-width: 100%; 440 | height: auto; 441 | } 442 | 443 | /* Make elements with the HTML hidden attribute stay hidden by default */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | 449 | * { 450 | --tw-border-opacity: 1; 451 | border-color: hsl(var(--border) / var(--tw-border-opacity)); 452 | } 453 | 454 | body { 455 | --tw-bg-opacity: 1; 456 | background-color: hsl(var(--background) / var(--tw-bg-opacity)); 457 | --tw-text-opacity: 1; 458 | color: hsl(var(--foreground) / var(--tw-text-opacity)); 459 | } 460 | 461 | :root { 462 | --background: 0 0% 100%; 463 | --foreground: 240 10% 3.9%; 464 | --card: 0 0% 100%; 465 | --card-foreground: 240 10% 3.9%; 466 | --popover: 0 0% 100%; 467 | --popover-foreground: 240 10% 3.9%; 468 | --primary: 240 5.9% 10%; 469 | --primary-foreground: 0 0% 98%; 470 | --secondary: 240 4.8% 95.9%; 471 | --secondary-foreground: 240 5.9% 10%; 472 | --muted: 240 4.8% 95.9%; 473 | --muted-foreground: 240 3.8% 46.1%; 474 | --accent: 240 4.8% 95.9%; 475 | --accent-foreground: 240 5.9% 10%; 476 | --destructive: 0 72.22% 50.59%; 477 | --destructive-foreground: 0 0% 98%; 478 | --border: 240 5.9% 90%; 479 | --input: 240 5.9% 90%; 480 | --ring: 240 5.9% 10%; 481 | --radius: 0.5rem; 482 | } 483 | 484 | .dark { 485 | --background: 240 10% 3.9%; 486 | --foreground: 0 0% 98%; 487 | --card: 240 10% 3.9%; 488 | --card-foreground: 0 0% 98%; 489 | --popover: 240 10% 3.9%; 490 | --popover-foreground: 0 0% 98%; 491 | --primary: 0 0% 98%; 492 | --primary-foreground: 240 5.9% 10%; 493 | --secondary: 240 3.7% 15.9%; 494 | --secondary-foreground: 0 0% 98%; 495 | --muted: 240 3.7% 15.9%; 496 | --muted-foreground: 240 5% 64.9%; 497 | --accent: 240 3.7% 15.9%; 498 | --accent-foreground: 0 0% 98%; 499 | --destructive: 0 62.8% 30.6%; 500 | --destructive-foreground: 0 0% 98%; 501 | --border: 240 3.7% 15.9%; 502 | --input: 240 3.7% 15.9%; 503 | --ring: 240 4.9% 83.9%; 504 | } 505 | 506 | *, ::before, ::after { 507 | --tw-border-spacing-x: 0; 508 | --tw-border-spacing-y: 0; 509 | --tw-translate-x: 0; 510 | --tw-translate-y: 0; 511 | --tw-rotate: 0; 512 | --tw-skew-x: 0; 513 | --tw-skew-y: 0; 514 | --tw-scale-x: 1; 515 | --tw-scale-y: 1; 516 | --tw-pan-x: ; 517 | --tw-pan-y: ; 518 | --tw-pinch-zoom: ; 519 | --tw-scroll-snap-strictness: proximity; 520 | --tw-gradient-from-position: ; 521 | --tw-gradient-via-position: ; 522 | --tw-gradient-to-position: ; 523 | --tw-ordinal: ; 524 | --tw-slashed-zero: ; 525 | --tw-numeric-figure: ; 526 | --tw-numeric-spacing: ; 527 | --tw-numeric-fraction: ; 528 | --tw-ring-inset: ; 529 | --tw-ring-offset-width: 0px; 530 | --tw-ring-offset-color: #fff; 531 | --tw-ring-color: rgb(59 130 246 / 0.5); 532 | --tw-ring-offset-shadow: 0 0 #0000; 533 | --tw-ring-shadow: 0 0 #0000; 534 | --tw-shadow: 0 0 #0000; 535 | --tw-shadow-colored: 0 0 #0000; 536 | --tw-blur: ; 537 | --tw-brightness: ; 538 | --tw-contrast: ; 539 | --tw-grayscale: ; 540 | --tw-hue-rotate: ; 541 | --tw-invert: ; 542 | --tw-saturate: ; 543 | --tw-sepia: ; 544 | --tw-drop-shadow: ; 545 | --tw-backdrop-blur: ; 546 | --tw-backdrop-brightness: ; 547 | --tw-backdrop-contrast: ; 548 | --tw-backdrop-grayscale: ; 549 | --tw-backdrop-hue-rotate: ; 550 | --tw-backdrop-invert: ; 551 | --tw-backdrop-opacity: ; 552 | --tw-backdrop-saturate: ; 553 | --tw-backdrop-sepia: ; 554 | --tw-contain-size: ; 555 | --tw-contain-layout: ; 556 | --tw-contain-paint: ; 557 | --tw-contain-style: ; 558 | } 559 | 560 | ::backdrop { 561 | --tw-border-spacing-x: 0; 562 | --tw-border-spacing-y: 0; 563 | --tw-translate-x: 0; 564 | --tw-translate-y: 0; 565 | --tw-rotate: 0; 566 | --tw-skew-x: 0; 567 | --tw-skew-y: 0; 568 | --tw-scale-x: 1; 569 | --tw-scale-y: 1; 570 | --tw-pan-x: ; 571 | --tw-pan-y: ; 572 | --tw-pinch-zoom: ; 573 | --tw-scroll-snap-strictness: proximity; 574 | --tw-gradient-from-position: ; 575 | --tw-gradient-via-position: ; 576 | --tw-gradient-to-position: ; 577 | --tw-ordinal: ; 578 | --tw-slashed-zero: ; 579 | --tw-numeric-figure: ; 580 | --tw-numeric-spacing: ; 581 | --tw-numeric-fraction: ; 582 | --tw-ring-inset: ; 583 | --tw-ring-offset-width: 0px; 584 | --tw-ring-offset-color: #fff; 585 | --tw-ring-color: rgb(59 130 246 / 0.5); 586 | --tw-ring-offset-shadow: 0 0 #0000; 587 | --tw-ring-shadow: 0 0 #0000; 588 | --tw-shadow: 0 0 #0000; 589 | --tw-shadow-colored: 0 0 #0000; 590 | --tw-blur: ; 591 | --tw-brightness: ; 592 | --tw-contrast: ; 593 | --tw-grayscale: ; 594 | --tw-hue-rotate: ; 595 | --tw-invert: ; 596 | --tw-saturate: ; 597 | --tw-sepia: ; 598 | --tw-drop-shadow: ; 599 | --tw-backdrop-blur: ; 600 | --tw-backdrop-brightness: ; 601 | --tw-backdrop-contrast: ; 602 | --tw-backdrop-grayscale: ; 603 | --tw-backdrop-hue-rotate: ; 604 | --tw-backdrop-invert: ; 605 | --tw-backdrop-opacity: ; 606 | --tw-backdrop-saturate: ; 607 | --tw-backdrop-sepia: ; 608 | --tw-contain-size: ; 609 | --tw-contain-layout: ; 610 | --tw-contain-paint: ; 611 | --tw-contain-style: ; 612 | } 613 | 614 | .container { 615 | width: 100%; 616 | margin-right: auto; 617 | margin-left: auto; 618 | padding-right: 2rem; 619 | padding-left: 2rem; 620 | } 621 | 622 | @media (min-width: 1400px) { 623 | .container { 624 | max-width: 1400px; 625 | } 626 | } 627 | 628 | .fixed { 629 | position: fixed; 630 | } 631 | 632 | .right-6 { 633 | right: 1.5rem; 634 | } 635 | 636 | .top-6 { 637 | top: 1.5rem; 638 | } 639 | 640 | .mx-auto { 641 | margin-left: auto; 642 | margin-right: auto; 643 | } 644 | 645 | .ml-4 { 646 | margin-left: 1rem; 647 | } 648 | 649 | .mt-10 { 650 | margin-top: 2.5rem; 651 | } 652 | 653 | .mt-32 { 654 | margin-top: 8rem; 655 | } 656 | 657 | .inline-block { 658 | display: inline-block; 659 | } 660 | 661 | .flex { 662 | display: flex; 663 | } 664 | 665 | .inline-flex { 666 | display: inline-flex; 667 | } 668 | 669 | .hidden { 670 | display: none; 671 | } 672 | 673 | .h-screen { 674 | height: 100vh; 675 | } 676 | 677 | .w-fit { 678 | width: -moz-fit-content; 679 | width: fit-content; 680 | } 681 | 682 | .w-full { 683 | width: 100%; 684 | } 685 | 686 | .max-w-2xl { 687 | max-width: 42rem; 688 | } 689 | 690 | .max-w-7xl { 691 | max-width: 80rem; 692 | } 693 | 694 | .max-w-md { 695 | max-width: 28rem; 696 | } 697 | 698 | .max-w-sm { 699 | max-width: 24rem; 700 | } 701 | 702 | .cursor-pointer { 703 | cursor: pointer; 704 | } 705 | 706 | .list-disc { 707 | list-style-type: disc; 708 | } 709 | 710 | .flex-col { 711 | flex-direction: column; 712 | } 713 | 714 | .items-center { 715 | align-items: center; 716 | } 717 | 718 | .justify-center { 719 | justify-content: center; 720 | } 721 | 722 | .justify-between { 723 | justify-content: space-between; 724 | } 725 | 726 | .gap-1 { 727 | gap: 0.25rem; 728 | } 729 | 730 | .gap-10 { 731 | gap: 2.5rem; 732 | } 733 | 734 | .gap-12 { 735 | gap: 3rem; 736 | } 737 | 738 | .gap-2 { 739 | gap: 0.5rem; 740 | } 741 | 742 | .gap-3 { 743 | gap: 0.75rem; 744 | } 745 | 746 | .gap-4 { 747 | gap: 1rem; 748 | } 749 | 750 | .gap-6 { 751 | gap: 1.5rem; 752 | } 753 | 754 | .gap-8 { 755 | gap: 2rem; 756 | } 757 | 758 | .rounded-md { 759 | border-radius: calc(var(--radius) - 2px); 760 | } 761 | 762 | .border { 763 | border-width: 1px; 764 | } 765 | 766 | .border-b { 767 | border-bottom-width: 1px; 768 | } 769 | 770 | .border-input { 771 | --tw-border-opacity: 1; 772 | border-color: hsl(var(--input) / var(--tw-border-opacity)); 773 | } 774 | 775 | .border-red-500 { 776 | --tw-border-opacity: 1; 777 | border-color: rgb(239 68 68 / var(--tw-border-opacity)); 778 | } 779 | 780 | .bg-primary { 781 | --tw-bg-opacity: 1; 782 | background-color: hsl(var(--primary) / var(--tw-bg-opacity)); 783 | } 784 | 785 | .bg-transparent { 786 | background-color: transparent; 787 | } 788 | 789 | .bg-gradient-to-r { 790 | background-image: linear-gradient(to right, var(--tw-gradient-stops)); 791 | } 792 | 793 | .from-indigo-500 { 794 | --tw-gradient-from: #6366f1 var(--tw-gradient-from-position); 795 | --tw-gradient-to: rgb(99 102 241 / 0) var(--tw-gradient-to-position); 796 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); 797 | } 798 | 799 | .via-purple-500 { 800 | --tw-gradient-to: rgb(168 85 247 / 0) var(--tw-gradient-to-position); 801 | --tw-gradient-stops: var(--tw-gradient-from), #a855f7 var(--tw-gradient-via-position), var(--tw-gradient-to); 802 | } 803 | 804 | .to-pink-500 { 805 | --tw-gradient-to: #ec4899 var(--tw-gradient-to-position); 806 | } 807 | 808 | .bg-clip-text { 809 | -webkit-background-clip: text; 810 | background-clip: text; 811 | } 812 | 813 | .px-3 { 814 | padding-left: 0.75rem; 815 | padding-right: 0.75rem; 816 | } 817 | 818 | .px-4 { 819 | padding-left: 1rem; 820 | padding-right: 1rem; 821 | } 822 | 823 | .px-6 { 824 | padding-left: 1.5rem; 825 | padding-right: 1.5rem; 826 | } 827 | 828 | .px-8 { 829 | padding-left: 2rem; 830 | padding-right: 2rem; 831 | } 832 | 833 | .py-12 { 834 | padding-top: 3rem; 835 | padding-bottom: 3rem; 836 | } 837 | 838 | .py-2 { 839 | padding-top: 0.5rem; 840 | padding-bottom: 0.5rem; 841 | } 842 | 843 | .py-3 { 844 | padding-top: 0.75rem; 845 | padding-bottom: 0.75rem; 846 | } 847 | 848 | .text-center { 849 | text-align: center; 850 | } 851 | 852 | .align-middle { 853 | vertical-align: middle; 854 | } 855 | 856 | .text-2xl { 857 | font-size: 1.5rem; 858 | line-height: 2rem; 859 | } 860 | 861 | .text-4xl { 862 | font-size: 2.25rem; 863 | line-height: 2.5rem; 864 | } 865 | 866 | .text-5xl { 867 | font-size: 3rem; 868 | line-height: 1; 869 | } 870 | 871 | .text-lg { 872 | font-size: 1.125rem; 873 | line-height: 1.75rem; 874 | } 875 | 876 | .text-sm { 877 | font-size: 0.875rem; 878 | line-height: 1.25rem; 879 | } 880 | 881 | .text-xl { 882 | font-size: 1.25rem; 883 | line-height: 1.75rem; 884 | } 885 | 886 | .text-xs { 887 | font-size: 0.75rem; 888 | line-height: 1rem; 889 | } 890 | 891 | .font-bold { 892 | font-weight: 700; 893 | } 894 | 895 | .font-medium { 896 | font-weight: 500; 897 | } 898 | 899 | .font-semibold { 900 | font-weight: 600; 901 | } 902 | 903 | .uppercase { 904 | text-transform: uppercase; 905 | } 906 | 907 | .tracking-wide { 908 | letter-spacing: 0.025em; 909 | } 910 | 911 | .text-foreground { 912 | --tw-text-opacity: 1; 913 | color: hsl(var(--foreground) / var(--tw-text-opacity)); 914 | } 915 | 916 | .text-muted-foreground { 917 | --tw-text-opacity: 1; 918 | color: hsl(var(--muted-foreground) / var(--tw-text-opacity)); 919 | } 920 | 921 | .text-primary-foreground { 922 | --tw-text-opacity: 1; 923 | color: hsl(var(--primary-foreground) / var(--tw-text-opacity)); 924 | } 925 | 926 | .text-red-500 { 927 | --tw-text-opacity: 1; 928 | color: rgb(239 68 68 / var(--tw-text-opacity)); 929 | } 930 | 931 | .text-transparent { 932 | color: transparent; 933 | } 934 | 935 | .underline { 936 | text-decoration-line: underline; 937 | } 938 | 939 | .shadow-sm { 940 | --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 941 | --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); 942 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 943 | } 944 | 945 | .ring-offset-background { 946 | --tw-ring-offset-color: hsl(var(--background) / 1); 947 | } 948 | 949 | .transition-colors { 950 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; 951 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 952 | transition-duration: 150ms; 953 | } 954 | 955 | .duration-200 { 956 | transition-duration: 200ms; 957 | } 958 | 959 | /* fix for Alpine users. */ 960 | 961 | [x-cloak] { 962 | display: none !important; 963 | } 964 | 965 | .placeholder\:text-neutral-500::-moz-placeholder { 966 | --tw-text-opacity: 1; 967 | color: rgb(115 115 115 / var(--tw-text-opacity)); 968 | } 969 | 970 | .placeholder\:text-neutral-500::placeholder { 971 | --tw-text-opacity: 1; 972 | color: rgb(115 115 115 / var(--tw-text-opacity)); 973 | } 974 | 975 | .hover\:bg-primary\/90:hover { 976 | background-color: hsl(var(--primary) / 0.9); 977 | } 978 | 979 | .focus\:border-neutral-300:focus { 980 | --tw-border-opacity: 1; 981 | border-color: rgb(212 212 212 / var(--tw-border-opacity)); 982 | } 983 | 984 | .focus\:outline-none:focus { 985 | outline: 2px solid transparent; 986 | outline-offset: 2px; 987 | } 988 | 989 | .focus\:ring:focus { 990 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); 991 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color); 992 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); 993 | } 994 | 995 | .focus\:ring-primary:focus { 996 | --tw-ring-opacity: 1; 997 | --tw-ring-color: hsl(var(--primary) / var(--tw-ring-opacity)); 998 | } 999 | 1000 | .disabled\:cursor-not-allowed:disabled { 1001 | cursor: not-allowed; 1002 | } 1003 | 1004 | .disabled\:opacity-50:disabled { 1005 | opacity: 0.5; 1006 | } 1007 | 1008 | @media (min-width: 1024px) { 1009 | .lg\:mt-20 { 1010 | margin-top: 5rem; 1011 | } 1012 | 1013 | .lg\:mt-32 { 1014 | margin-top: 8rem; 1015 | } 1016 | 1017 | .lg\:mt-40 { 1018 | margin-top: 10rem; 1019 | } 1020 | 1021 | .lg\:text-4xl { 1022 | font-size: 2.25rem; 1023 | line-height: 2.5rem; 1024 | } 1025 | 1026 | .lg\:text-7xl { 1027 | font-size: 4.5rem; 1028 | line-height: 1; 1029 | } 1030 | 1031 | .lg\:leading-tight { 1032 | line-height: 1.25; 1033 | } 1034 | } 1035 | -------------------------------------------------------------------------------- /bootstrap/public/public.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import "embed" 4 | 5 | //go:embed assets 6 | var AssetsFS embed.FS 7 | -------------------------------------------------------------------------------- /bootstrap/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { fontFamily } from "tailwindcss/defaultTheme"; 2 | 3 | module.exports = { 4 | content: [ "./**/*.html", "./**/*.templ", "./**/*.go", ], 5 | safelist: [], 6 | theme: { 7 | container: { 8 | center: true, 9 | padding: "2rem", 10 | screens: { 11 | "2xl": "1400px" 12 | } 13 | }, 14 | extend: { 15 | colors: { 16 | border: "hsl(var(--border) / )", 17 | input: "hsl(var(--input) / )", 18 | ring: "hsl(var(--ring) / )", 19 | background: "hsl(var(--background) / )", 20 | foreground: "hsl(var(--foreground) / )", 21 | primary: { 22 | DEFAULT: "hsl(var(--primary) / )", 23 | foreground: "hsl(var(--primary-foreground) / )" 24 | }, 25 | secondary: { 26 | DEFAULT: "hsl(var(--secondary) / )", 27 | foreground: "hsl(var(--secondary-foreground) / )" 28 | }, 29 | destructive: { 30 | DEFAULT: "hsl(var(--destructive) / )", 31 | foreground: "hsl(var(--destructive-foreground) / )" 32 | }, 33 | muted: { 34 | DEFAULT: "hsl(var(--muted) / )", 35 | foreground: "hsl(var(--muted-foreground) / )" 36 | }, 37 | accent: { 38 | DEFAULT: "hsl(var(--accent) / )", 39 | foreground: "hsl(var(--accent-foreground) / )" 40 | }, 41 | popover: { 42 | DEFAULT: "hsl(var(--popover) / )", 43 | foreground: "hsl(var(--popover-foreground) / )" 44 | }, 45 | card: { 46 | DEFAULT: "hsl(var(--card) / )", 47 | foreground: "hsl(var(--card-foreground) / )" 48 | } 49 | }, 50 | borderRadius: { 51 | lg: "var(--radius)", 52 | md: "calc(var(--radius) - 2px)", 53 | sm: "calc(var(--radius) - 4px)" 54 | }, 55 | fontFamily: { 56 | sans: [...fontFamily.sans] 57 | } 58 | } 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | DriverSqlite3 = "sqlite3" 10 | DriverMysql = "mysql" 11 | ) 12 | 13 | type Config struct { 14 | Driver string 15 | Name string 16 | Host string 17 | User string 18 | Password string 19 | } 20 | 21 | func NewSQL(cfg Config) (*sql.DB, error) { 22 | switch cfg.Driver { 23 | case DriverSqlite3: 24 | name := cfg.Name 25 | if len(name) == 0 { 26 | name = "app_db" 27 | } 28 | return sql.Open(cfg.Driver, name) 29 | default: 30 | return nil, fmt.Errorf("invalid database driver (%s): currently only sqlite3 is supported", cfg.Driver) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // HandlerFunc is the function being called when receiving an event. 11 | type HandlerFunc func(context.Context, any) 12 | 13 | // Emit and event to the given topic 14 | func Emit(topic string, event any) { 15 | stream.emit(topic, event) 16 | } 17 | 18 | // Subscribe a HandlerFunc to the given topic. 19 | // A Subscription is being returned that can be used 20 | // to unsubscribe from the topic. 21 | func Subscribe(topic string, h HandlerFunc) Subscription { 22 | return stream.subscribe(topic, h) 23 | } 24 | 25 | // Unsubscribe unsubribes the given Subscription from its topic. 26 | func Unsubscribe(sub Subscription) { 27 | stream.unsubscribe(sub) 28 | } 29 | 30 | // Stop stops the event stream, cleaning up its resources. 31 | func Stop() { 32 | stream.stop() 33 | } 34 | 35 | var stream *eventStream 36 | 37 | type event struct { 38 | topic string 39 | message any 40 | } 41 | 42 | // Subscription represents a handler subscribed to a specific topic. 43 | type Subscription struct { 44 | Topic string 45 | CreatedAt int64 46 | Fn HandlerFunc 47 | } 48 | 49 | type eventStream struct { 50 | mu sync.RWMutex 51 | subs map[string][]Subscription 52 | eventch chan event 53 | quitch chan struct{} 54 | } 55 | 56 | func newStream() *eventStream { 57 | e := &eventStream{ 58 | subs: make(map[string][]Subscription), 59 | eventch: make(chan event, 128), 60 | quitch: make(chan struct{}), 61 | } 62 | go e.start() 63 | return e 64 | } 65 | 66 | func (e *eventStream) start() { 67 | ctx := context.Background() 68 | for { 69 | select { 70 | case <-e.quitch: 71 | return 72 | case evt := <-e.eventch: 73 | if handlers, ok := e.subs[evt.topic]; ok { 74 | for _, sub := range handlers { 75 | go sub.Fn(ctx, evt.message) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | func (e *eventStream) stop() { 83 | e.quitch <- struct{}{} 84 | } 85 | 86 | func (e *eventStream) emit(topic string, v any) { 87 | e.eventch <- event{ 88 | topic: topic, 89 | message: v, 90 | } 91 | } 92 | 93 | func (e *eventStream) subscribe(topic string, h HandlerFunc) Subscription { 94 | e.mu.RLock() 95 | defer e.mu.RUnlock() 96 | 97 | sub := Subscription{ 98 | CreatedAt: time.Now().UnixNano(), 99 | Topic: topic, 100 | Fn: h, 101 | } 102 | 103 | if _, ok := e.subs[topic]; !ok { 104 | e.subs[topic] = []Subscription{} 105 | } 106 | 107 | e.subs[topic] = append(e.subs[topic], sub) 108 | 109 | return sub 110 | } 111 | 112 | func (e *eventStream) unsubscribe(sub Subscription) { 113 | e.mu.RLock() 114 | defer e.mu.RUnlock() 115 | 116 | if _, ok := e.subs[sub.Topic]; ok { 117 | e.subs[sub.Topic] = slices.DeleteFunc(e.subs[sub.Topic], func(e Subscription) bool { 118 | return sub.CreatedAt == e.CreatedAt 119 | }) 120 | } 121 | if len(e.subs[sub.Topic]) == 0 { 122 | delete(e.subs, sub.Topic) 123 | } 124 | } 125 | 126 | func init() { 127 | stream = newStream() 128 | } 129 | -------------------------------------------------------------------------------- /event/event_test.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestEventSubscribeEmit(t *testing.T) { 10 | expect := 1 11 | ctx, cancel := context.WithCancel(context.Background()) 12 | Subscribe("foo.a", func(_ context.Context, event any) { 13 | defer cancel() 14 | value, ok := event.(int) 15 | if !ok { 16 | t.Errorf("expected int got %v", reflect.TypeOf(event)) 17 | } 18 | if value != 1 { 19 | t.Errorf("expected %d got %d", expect, value) 20 | } 21 | }) 22 | Emit("foo.a", expect) 23 | <-ctx.Done() 24 | } 25 | 26 | func TestUnsubscribe(t *testing.T) { 27 | sub := Subscribe("foo.b", func(_ context.Context, _ any) {}) 28 | Unsubscribe(sub) 29 | if _, ok := stream.subs["foo.b"]; ok { 30 | t.Errorf("expected topic foo.bar to be deleted") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/anthdm/superkit 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/a-h/templ v0.2.731 7 | github.com/gorilla/sessions v1.3.0 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/gorilla/securecookie v1.1.2 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= 2 | github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= 3 | github.com/a-h/templ v0.2.731 h1:yiv4C7whSUsa36y65O06DPr/U/j3+WGB0RmvLOoVFXc= 4 | github.com/a-h/templ v0.2.731/go.mod h1:IejA/ecDD0ul0dCvgCwp9t7bUZXVpGClEAdsqZQigi8= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 8 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 10 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 11 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 12 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 13 | github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= 14 | github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 18 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "fmt" 7 | "io/fs" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "path" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | const ( 17 | replaceID = "AABBCCDD" 18 | bootstrapFolderName = "bootstrap" 19 | reponame = "https://github.com/anthdm/superkit.git" 20 | ) 21 | 22 | func main() { 23 | args := os.Args[1:] 24 | 25 | if len(args) == 0 { 26 | fmt.Println() 27 | fmt.Println("install requires your project name as the first argument") 28 | fmt.Println() 29 | fmt.Println("\tgo run superkit/install.go [your_project_name]") 30 | fmt.Println() 31 | os.Exit(1) 32 | } 33 | 34 | projectName := args[0] 35 | 36 | // check if superkit folder already exists, if so, delete 37 | _, err := os.Stat("superkit") 38 | if !os.IsNotExist(err) { 39 | fmt.Println("-- deleting superkit folder cause its already present") 40 | if err := os.RemoveAll("superkit"); err != nil { 41 | log.Fatal(err) 42 | } 43 | } 44 | 45 | fmt.Println("-- cloning", reponame) 46 | clone := exec.Command("git", "clone", reponame) 47 | if err := clone.Run(); err != nil { 48 | log.Fatal(err) 49 | } 50 | 51 | fmt.Println("-- renaming bootstrap ->", projectName) 52 | if err := os.Rename(path.Join("superkit", bootstrapFolderName), projectName); err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | err = filepath.Walk(path.Join(projectName), func(fullPath string, info fs.FileInfo, err error) error { 57 | if err != nil { 58 | return err 59 | } 60 | if info.IsDir() { 61 | return nil 62 | } 63 | 64 | b, err := os.ReadFile(fullPath) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | contentStr := string(b) 70 | if strings.Contains(contentStr, replaceID) { 71 | replacedContent := strings.ReplaceAll(contentStr, replaceID, projectName) 72 | file, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_TRUNC, 0644) 73 | if err != nil { 74 | return err 75 | } 76 | defer file.Close() 77 | _, err = file.WriteString(replacedContent) 78 | if err != nil { 79 | return err 80 | } 81 | } 82 | return nil 83 | }) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | 88 | fmt.Println("-- renaming .env.local -> .env") 89 | if err := os.Rename( 90 | path.Join(projectName, ".env.local"), 91 | path.Join(projectName, ".env"), 92 | ); err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | fmt.Println("-- generating secure secret") 97 | pathToDotEnv := path.Join(projectName, ".env") 98 | b, err := os.ReadFile(pathToDotEnv) 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | secret := generateSecret() 103 | replacedContent := strings.Replace(string(b), "{{app_secret}}", secret, -1) 104 | file, err := os.OpenFile(pathToDotEnv, os.O_WRONLY|os.O_TRUNC, 0644) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | defer file.Close() 109 | _, err = file.WriteString(replacedContent) 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | fmt.Printf("-- project (%s) successfully installed!\n", projectName) 114 | } 115 | 116 | func generateSecret() string { 117 | bytes := make([]byte, 32) 118 | if _, err := rand.Read(bytes); err != nil { 119 | log.Fatal(err) 120 | } 121 | return hex.EncodeToString(bytes) 122 | } 123 | -------------------------------------------------------------------------------- /kit/kit.go: -------------------------------------------------------------------------------- 1 | package kit 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/a-h/templ" 13 | "github.com/gorilla/sessions" 14 | "github.com/joho/godotenv" 15 | ) 16 | 17 | var store *sessions.CookieStore 18 | 19 | type HandlerFunc func(kit *Kit) error 20 | 21 | type ErrorHandlerFunc func(kit *Kit, err error) 22 | 23 | type AuthKey struct{} 24 | 25 | type Auth interface { 26 | Check() bool 27 | } 28 | 29 | var ( 30 | errorHandler = func(kit *Kit, err error) { 31 | kit.Text(http.StatusInternalServerError, err.Error()) 32 | } 33 | ) 34 | 35 | type DefaultAuth struct{} 36 | 37 | func (DefaultAuth) Check() bool { return false } 38 | 39 | type Kit struct { 40 | Response http.ResponseWriter 41 | Request *http.Request 42 | } 43 | 44 | func UseErrorHandler(h ErrorHandlerFunc) { errorHandler = h } 45 | 46 | func (kit *Kit) Auth() Auth { 47 | value, ok := kit.Request.Context().Value(AuthKey{}).(Auth) 48 | if !ok { 49 | slog.Warn("kit authentication not set") 50 | return DefaultAuth{} 51 | } 52 | return value 53 | } 54 | 55 | // GetSession return a session by its name. GetSession always 56 | // returns a session even if it does not exist. 57 | func (kit *Kit) GetSession(name string) *sessions.Session { 58 | sess, _ := store.Get(kit.Request, name) 59 | return sess 60 | } 61 | 62 | // Redirect with HTMX support. 63 | func (kit *Kit) Redirect(status int, url string) error { 64 | if len(kit.Request.Header.Get("HX-Request")) > 0 { 65 | kit.Response.Header().Set("HX-Redirect", url) 66 | kit.Response.WriteHeader(http.StatusSeeOther) 67 | return nil 68 | } 69 | http.Redirect(kit.Response, kit.Request, url, status) 70 | return nil 71 | } 72 | 73 | func (kit *Kit) FormValue(name string) string { 74 | return kit.Request.PostFormValue(name) 75 | } 76 | 77 | func (kit *Kit) JSON(status int, v any) error { 78 | kit.Response.WriteHeader(status) 79 | kit.Response.Header().Set("Content-Type", "application/json") 80 | return json.NewEncoder(kit.Response).Encode(v) 81 | } 82 | 83 | func (kit *Kit) Text(status int, msg string) error { 84 | kit.Response.WriteHeader(status) 85 | kit.Response.Header().Set("Content-Type", "text/plain") 86 | _, err := kit.Response.Write([]byte(msg)) 87 | return err 88 | } 89 | 90 | func (kit *Kit) Bytes(status int, b []byte) error { 91 | kit.Response.WriteHeader(status) 92 | kit.Response.Header().Set("Content-Type", "text/plain") 93 | _, err := kit.Response.Write(b) 94 | return err 95 | } 96 | 97 | func (kit *Kit) Render(c templ.Component) error { 98 | return c.Render(kit.Request.Context(), kit.Response) 99 | } 100 | 101 | func (kit *Kit) Getenv(name string, def string) string { 102 | return Getenv(name, def) 103 | } 104 | 105 | func Handler(h HandlerFunc) http.HandlerFunc { 106 | return func(w http.ResponseWriter, r *http.Request) { 107 | kit := &Kit{ 108 | Response: w, 109 | Request: r, 110 | } 111 | if err := h(kit); err != nil { 112 | if errorHandler != nil { 113 | errorHandler(kit, err) 114 | return 115 | } 116 | kit.Text(http.StatusInternalServerError, err.Error()) 117 | } 118 | } 119 | } 120 | 121 | type AuthenticationConfig struct { 122 | AuthFunc func(*Kit) (Auth, error) 123 | RedirectURL string 124 | } 125 | 126 | func WithAuthentication(config AuthenticationConfig, strict bool) func(http.Handler) http.Handler { 127 | return func(next http.Handler) http.Handler { 128 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 129 | kit := &Kit{ 130 | Response: w, 131 | Request: r, 132 | } 133 | auth, err := config.AuthFunc(kit) 134 | if err != nil { 135 | errorHandler(kit, err) 136 | return 137 | } 138 | if strict && !auth.Check() && r.URL.Path != config.RedirectURL { 139 | kit.Redirect(http.StatusSeeOther, config.RedirectURL) 140 | return 141 | } 142 | ctx := context.WithValue(r.Context(), AuthKey{}, auth) 143 | next.ServeHTTP(w, r.WithContext(ctx)) 144 | }) 145 | } 146 | } 147 | 148 | func Getenv(name string, def string) string { 149 | env := os.Getenv(name) 150 | if len(env) == 0 { 151 | return def 152 | } 153 | return env 154 | } 155 | 156 | func IsDevelopment() bool { 157 | return os.Getenv("SUPERKIT_ENV") == "development" 158 | } 159 | 160 | func IsProduction() bool { 161 | return os.Getenv("SUPERKIT_ENV") == "production" 162 | } 163 | 164 | func Env() string { 165 | return os.Getenv("SUPERKIT_ENV") 166 | } 167 | 168 | // initialize the store here so the environment variables are 169 | // already initialized. Calling NewCookieStore() from outside of 170 | // a function scope won't work. 171 | func Setup() { 172 | if err := godotenv.Load(); err != nil { 173 | log.Fatal(err) 174 | } 175 | appSecret := os.Getenv("SUPERKIT_SECRET") 176 | if len(appSecret) < 32 { 177 | // For security reasons we are calling os.Exit(1) here so Go's panic recover won't 178 | // recover the application without a valid SUPERKIT_SECRET set. 179 | fmt.Println("invalid SUPERKIT_SECRET variable. Are you sure you have set the SUPERKIT_SECRET in your .env file?") 180 | os.Exit(1) 181 | } 182 | store = sessions.NewCookieStore([]byte(appSecret)) 183 | } 184 | -------------------------------------------------------------------------------- /kit/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type ( 9 | RequestKey struct{} 10 | ResponseHeadersKey struct{} 11 | ) 12 | 13 | func WithRequest(next http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | ctx := context.WithValue(r.Context(), RequestKey{}, r) 16 | next.ServeHTTP(w, r.WithContext(ctx)) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /ui/button/button.go: -------------------------------------------------------------------------------- 1 | package button 2 | 3 | import ( 4 | "github.com/anthdm/superkit/ui" 5 | 6 | "github.com/a-h/templ" 7 | ) 8 | 9 | const ( 10 | buttonBaseClass = "inline-flex items-center justify-center px-4 py-2 font-medium text-sm tracking-wide transition-colors duration-200 rounded-md focus:ring focus:shadow-outline focus:outline-none" 11 | buttonVariantPrimary = "text-primary-foreground bg-primary focus:ring-primary hover:bg-primary/90" 12 | buttonVariantOutline = "text-primary border border-primary hover:bg-secondary focus:ring-primary" 13 | buttonVariantSecondary = "text-primary bg-secondary hover:bg-secondary/80" 14 | buttonVariantDestructive = "text-primary bg-destructive hover:bg-destructive/80" 15 | ) 16 | 17 | func New(opts ...func(*templ.Attributes)) templ.Attributes { 18 | return ui.CreateAttrs(buttonBaseClass, buttonVariantPrimary, opts...) 19 | } 20 | 21 | func Outline(opts ...func(*templ.Attributes)) templ.Attributes { 22 | return appendVariant("outline", opts...) 23 | } 24 | 25 | func Primary(opts ...func(*templ.Attributes)) templ.Attributes { 26 | return appendVariant("primary", opts...) 27 | } 28 | 29 | func Secondary(opts ...func(*templ.Attributes)) templ.Attributes { 30 | return appendVariant("secondary", opts...) 31 | } 32 | 33 | func Destructive(opts ...func(*templ.Attributes)) templ.Attributes { 34 | return appendVariant("destructive", opts...) 35 | } 36 | 37 | func Variant(variant string) func(*templ.Attributes) { 38 | return func(attrs *templ.Attributes) { 39 | att := *attrs 40 | switch variant { 41 | case "primary": 42 | att["class"] = ui.Merge(buttonBaseClass, buttonVariantPrimary) 43 | case "outline": 44 | att["class"] = ui.Merge(buttonBaseClass, buttonVariantOutline) 45 | case "secondary": 46 | att["class"] = ui.Merge(buttonBaseClass, buttonVariantSecondary) 47 | case "destructive": 48 | att["class"] = ui.Merge(buttonBaseClass, buttonVariantDestructive) 49 | } 50 | } 51 | } 52 | 53 | func appendVariant(variant string, opts ...func(*templ.Attributes)) templ.Attributes { 54 | opt := []func(*templ.Attributes){ 55 | Variant(variant), 56 | } 57 | opt = append(opt, opts...) 58 | return New(opt...) 59 | } 60 | -------------------------------------------------------------------------------- /ui/card/card.templ: -------------------------------------------------------------------------------- 1 | package card 2 | 3 | import "github.com/anthdm/superkit/ui" 4 | 5 | const cardBaseClass = "rounded-lg border bg-card text-card-foreground shadow-sm" 6 | 7 | templ Card(opts ...func(*templ.Attributes)) { 8 |
9 | { children... } 10 |
11 | } 12 | 13 | templ Header(opts ...func(*templ.Attributes)) { 14 |
15 | { children... } 16 |
17 | } 18 | 19 | templ Content(opts ...func(*templ.Attributes)) { 20 |
21 | { children... } 22 |
23 | } 24 | 25 | templ Footer(opts ...func(*templ.Attributes)) { 26 |
27 | { children... } 28 |
29 | } 30 | -------------------------------------------------------------------------------- /ui/input/input.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "github.com/a-h/templ" 5 | 6 | "github.com/anthdm/superkit/ui" 7 | ) 8 | 9 | const defaultInputClass = "flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" 10 | 11 | func Input(opts ...func(*templ.Attributes)) templ.Attributes { 12 | return ui.CreateAttrs(defaultInputClass, "", opts...) 13 | } 14 | -------------------------------------------------------------------------------- /ui/modal/modal.templ: -------------------------------------------------------------------------------- 1 | package modal 2 | 3 | templ Modal() { 4 |
9 |
10 | foo 11 |
12 | 43 |
44 | } 45 | 46 | templ Header() { 47 |
48 |

49 | { children... } 50 |

51 | 54 |
55 | } 56 | 57 | templ Content() { 58 |
59 | { children... } 60 |
61 | } 62 | 63 | templ Trigger() { 64 |
65 | { children... } 66 |
67 | } 68 | 69 | templ Footer() { 70 | } 71 | -------------------------------------------------------------------------------- /ui/table/table.templ: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "github.com/anthdm/superkit/ui" 5 | ) 6 | 7 | templ Table() { 8 |
9 |
10 |
11 |
12 | 13 | { children... } 14 |
15 |
16 |
17 |
18 |
19 | } 20 | 21 | templ Header(opts ...func(*templ.Attributes)) { 22 | 23 | 24 | { children... } 25 | 26 | 27 | } 28 | 29 | templ Body(opts ...func(*templ.Attributes)) { 30 | 31 | { children... } 32 | 33 | } 34 | 35 | templ Footer(opts ...func(*templ.Attributes)) { 36 | 37 | 38 | { children... } 39 | 40 | 41 | } 42 | 43 | const ( 44 | thBaseClass = "px-5 py-3 text-xs font-medium uppercase" 45 | tdBaseClass = "px-5 py-4 text-sm whitespace-nowrap" 46 | ) 47 | 48 | func Td(opts ...func(*templ.Attributes)) templ.Attributes { 49 | return ui.CreateAttrs(tdBaseClass, "", opts...) 50 | } 51 | 52 | func Th(opts ...func(*templ.Attributes)) templ.Attributes { 53 | return ui.CreateAttrs(thBaseClass, "", opts...) 54 | } 55 | -------------------------------------------------------------------------------- /ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/a-h/templ" 7 | ) 8 | 9 | func CreateAttrs(baseClass string, defaultClass string, opts ...func(*templ.Attributes)) templ.Attributes { 10 | attrs := templ.Attributes{ 11 | "class": baseClass + " " + defaultClass, 12 | } 13 | for _, o := range opts { 14 | o(&attrs) 15 | } 16 | return attrs 17 | } 18 | 19 | func Merge(a, b string) string { 20 | return fmt.Sprintf("%s %s", a, b) 21 | } 22 | 23 | func Class(class string) func(*templ.Attributes) { 24 | return func(attrs *templ.Attributes) { 25 | attr := *attrs 26 | class := attr["class"].(string) + " " + class 27 | attr["class"] = class 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /validate/README.md: -------------------------------------------------------------------------------- 1 | # Validate 2 | Schema based validation with superpowers for Golang. 3 | 4 | # TODO 5 | -------------------------------------------------------------------------------- /validate/rules.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "time" 8 | "unicode" 9 | ) 10 | 11 | var ( 12 | emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) 13 | urlRegex = regexp.MustCompile(`^(https?:\/\/)?(www\.)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]*)?$`) 14 | ) 15 | 16 | // RuleSet holds the state of a single rule. 17 | type RuleSet struct { 18 | Name string 19 | RuleValue any 20 | FieldValue any 21 | FieldName any 22 | ErrorMessage string 23 | MessageFunc func(RuleSet) string 24 | ValidateFunc func(RuleSet) bool 25 | } 26 | 27 | // Message overrides the default message of a RuleSet 28 | func (set RuleSet) Message(msg string) RuleSet { 29 | set.ErrorMessage = msg 30 | return set 31 | } 32 | 33 | type Numeric interface { 34 | int | float64 35 | } 36 | 37 | func In[T any](values []T) RuleSet { 38 | return RuleSet{ 39 | Name: "in", 40 | RuleValue: values, 41 | ValidateFunc: func(set RuleSet) bool { 42 | for _, value := range values { 43 | v := set.FieldValue.(T) 44 | if reflect.DeepEqual(v, value) { 45 | return true 46 | } 47 | } 48 | return false 49 | }, 50 | MessageFunc: func(set RuleSet) string { 51 | return fmt.Sprintf("should be in %v", values) 52 | }, 53 | } 54 | } 55 | 56 | var ContainsUpper = RuleSet{ 57 | Name: "containsUpper", 58 | ValidateFunc: func(rule RuleSet) bool { 59 | str, ok := rule.FieldValue.(string) 60 | if !ok { 61 | return false 62 | } 63 | for _, ch := range str { 64 | if unicode.IsUpper(rune(ch)) { 65 | return true 66 | } 67 | } 68 | return false 69 | }, 70 | MessageFunc: func(set RuleSet) string { 71 | return "must contain at least 1 uppercase character" 72 | }, 73 | } 74 | 75 | var ContainsDigit = RuleSet{ 76 | Name: "containsDigit", 77 | ValidateFunc: func(rule RuleSet) bool { 78 | str, ok := rule.FieldValue.(string) 79 | if !ok { 80 | return false 81 | } 82 | return hasDigit(str) 83 | }, 84 | MessageFunc: func(set RuleSet) string { 85 | return "must contain at least 1 numeric character" 86 | }, 87 | } 88 | 89 | var ContainsSpecial = RuleSet{ 90 | Name: "containsSpecial", 91 | ValidateFunc: func(rule RuleSet) bool { 92 | str, ok := rule.FieldValue.(string) 93 | if !ok { 94 | return false 95 | } 96 | return hasSpecialChar(str) 97 | }, 98 | MessageFunc: func(set RuleSet) string { 99 | return "must contain at least 1 special character" 100 | }, 101 | } 102 | 103 | var Required = RuleSet{ 104 | Name: "required", 105 | MessageFunc: func(set RuleSet) string { 106 | return "is a required field" 107 | }, 108 | ValidateFunc: func(rule RuleSet) bool { 109 | str, ok := rule.FieldValue.(string) 110 | if !ok { 111 | return false 112 | } 113 | return len(str) > 0 114 | }, 115 | } 116 | 117 | var URL = RuleSet{ 118 | Name: "url", 119 | MessageFunc: func(set RuleSet) string { 120 | return "is not a valid url" 121 | }, 122 | ValidateFunc: func(set RuleSet) bool { 123 | u, ok := set.FieldValue.(string) 124 | if !ok { 125 | return false 126 | } 127 | return urlRegex.MatchString(u) 128 | }, 129 | } 130 | 131 | var Email = RuleSet{ 132 | Name: "email", 133 | MessageFunc: func(set RuleSet) string { 134 | return "is not a valid email address" 135 | }, 136 | ValidateFunc: func(set RuleSet) bool { 137 | email, ok := set.FieldValue.(string) 138 | if !ok { 139 | return false 140 | } 141 | return emailRegex.MatchString(email) 142 | }, 143 | } 144 | 145 | var Time = RuleSet{ 146 | Name: "time", 147 | ValidateFunc: func(set RuleSet) bool { 148 | t, ok := set.FieldValue.(time.Time) 149 | if !ok { 150 | return false 151 | } 152 | return t.After(time.Time{}) 153 | }, 154 | MessageFunc: func(set RuleSet) string { 155 | return "is not a valid time" 156 | }, 157 | } 158 | 159 | func TimeAfter(t time.Time) RuleSet { 160 | return RuleSet{ 161 | Name: "timeAfter", 162 | ValidateFunc: func(set RuleSet) bool { 163 | t, ok := set.FieldValue.(time.Time) 164 | if !ok { 165 | return false 166 | } 167 | return t.After(t) 168 | }, 169 | MessageFunc: func(set RuleSet) string { 170 | return fmt.Sprintf("is not after %v", set.FieldValue) 171 | }, 172 | } 173 | } 174 | 175 | func TimeBefore(t time.Time) RuleSet { 176 | return RuleSet{ 177 | Name: "timeBefore", 178 | ValidateFunc: func(set RuleSet) bool { 179 | t, ok := set.FieldValue.(time.Time) 180 | if !ok { 181 | return false 182 | } 183 | return t.Before(t) 184 | }, 185 | MessageFunc: func(set RuleSet) string { 186 | return fmt.Sprintf("is not before %v", set.FieldValue) 187 | }, 188 | } 189 | } 190 | 191 | func EQ[T comparable](v T) RuleSet { 192 | return RuleSet{ 193 | Name: "eq", 194 | RuleValue: v, 195 | ValidateFunc: func(set RuleSet) bool { 196 | return set.FieldValue.(T) == v 197 | }, 198 | MessageFunc: func(set RuleSet) string { 199 | return fmt.Sprintf("should be equal to %v", v) 200 | }, 201 | } 202 | } 203 | 204 | func LTE[T Numeric](n T) RuleSet { 205 | return RuleSet{ 206 | Name: "lte", 207 | RuleValue: n, 208 | ValidateFunc: func(set RuleSet) bool { 209 | return set.FieldValue.(T) <= n 210 | }, 211 | MessageFunc: func(set RuleSet) string { 212 | return fmt.Sprintf("should be lesser or equal than %v", n) 213 | }, 214 | } 215 | } 216 | 217 | func GTE[T Numeric](n T) RuleSet { 218 | return RuleSet{ 219 | Name: "gte", 220 | RuleValue: n, 221 | ValidateFunc: func(set RuleSet) bool { 222 | return set.FieldValue.(T) >= n 223 | }, 224 | MessageFunc: func(set RuleSet) string { 225 | return fmt.Sprintf("should be greater or equal than %v", n) 226 | }, 227 | } 228 | } 229 | 230 | func LT[T Numeric](n T) RuleSet { 231 | return RuleSet{ 232 | Name: "lt", 233 | RuleValue: n, 234 | ValidateFunc: func(set RuleSet) bool { 235 | return set.FieldValue.(T) < n 236 | }, 237 | MessageFunc: func(set RuleSet) string { 238 | return fmt.Sprintf("should be lesser than %v", n) 239 | }, 240 | } 241 | } 242 | 243 | func GT[T Numeric](n T) RuleSet { 244 | return RuleSet{ 245 | Name: "gt", 246 | RuleValue: n, 247 | ValidateFunc: func(set RuleSet) bool { 248 | return set.FieldValue.(T) > n 249 | }, 250 | MessageFunc: func(set RuleSet) string { 251 | return fmt.Sprintf("should be greater than %v", n) 252 | }, 253 | } 254 | } 255 | 256 | func Max(n int) RuleSet { 257 | return RuleSet{ 258 | Name: "max", 259 | RuleValue: n, 260 | ValidateFunc: func(set RuleSet) bool { 261 | str, ok := set.FieldValue.(string) 262 | if !ok { 263 | return false 264 | } 265 | return len(str) <= n 266 | }, 267 | MessageFunc: func(set RuleSet) string { 268 | return fmt.Sprintf("should be maximum %d characters long", n) 269 | }, 270 | } 271 | } 272 | 273 | func Min(n int) RuleSet { 274 | return RuleSet{ 275 | Name: "min", 276 | RuleValue: n, 277 | ValidateFunc: func(set RuleSet) bool { 278 | str, ok := set.FieldValue.(string) 279 | if !ok { 280 | return false 281 | } 282 | return len(str) >= n 283 | }, 284 | MessageFunc: func(set RuleSet) string { 285 | return fmt.Sprintf("should be at least %d characters long", n) 286 | }, 287 | } 288 | } 289 | 290 | func hasDigit(s string) bool { 291 | for _, char := range s { 292 | if unicode.IsDigit(char) { 293 | return true 294 | } 295 | } 296 | return false 297 | } 298 | 299 | func hasSpecialChar(s string) bool { 300 | for _, char := range s { 301 | if !unicode.IsLetter(char) && !unicode.IsDigit(char) { 302 | return true 303 | } 304 | } 305 | return false 306 | } 307 | -------------------------------------------------------------------------------- /validate/validate.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | "net/http" 7 | "reflect" 8 | "strconv" 9 | "unicode" 10 | ) 11 | 12 | // Errors is a map holding all the possible errors that may 13 | // occur during validation. 14 | type Errors map[string][]string 15 | 16 | // Any return true if there is any error. 17 | func (e Errors) Any() bool { 18 | return len(e) > 0 19 | } 20 | 21 | // Add adds an error for a specific field 22 | func (e Errors) Add(field string, msg string) { 23 | if _, ok := e[field]; !ok { 24 | e[field] = []string{} 25 | } 26 | e[field] = append(e[field], msg) 27 | } 28 | 29 | // Get returns all the errors for the given field. 30 | func (e Errors) Get(field string) []string { 31 | return e[field] 32 | } 33 | 34 | // Has returns true whether the given field has any errors. 35 | func (e Errors) Has(field string) bool { 36 | return len(e[field]) > 0 37 | } 38 | 39 | // Schema represents a validation schema. 40 | type Schema map[string][]RuleSet 41 | 42 | // Merge merges the two given schemas, returning a new Schema. 43 | func Merge(schema, other Schema) Schema { 44 | newSchema := Schema{} 45 | maps.Copy(newSchema, schema) 46 | maps.Copy(newSchema, other) 47 | return newSchema 48 | } 49 | 50 | // Rules is a function that takes any amount of RuleSets 51 | func Rules(rules ...RuleSet) []RuleSet { 52 | ruleSets := make([]RuleSet, len(rules)) 53 | for i := 0; i < len(ruleSets); i++ { 54 | ruleSets[i] = rules[i] 55 | } 56 | return ruleSets 57 | } 58 | 59 | // Validate validates data based on the given Schema. 60 | func Validate(data any, fields Schema) (Errors, bool) { 61 | errors := Errors{} 62 | return validate(data, fields, errors) 63 | } 64 | 65 | // Request parses an http.Request into data and validates it based 66 | // on the given schema. 67 | func Request(r *http.Request, data any, schema Schema) (Errors, bool) { 68 | errors := Errors{} 69 | if err := parseRequest(r, data); err != nil { 70 | errors["_error"] = []string{err.Error()} 71 | } 72 | return validate(data, schema, errors) 73 | } 74 | 75 | func validate(data any, schema Schema, errors Errors) (Errors, bool) { 76 | ok := true 77 | for fieldName, ruleSets := range schema { 78 | // Uppercase the field name so we never check un-exported fields. 79 | // But we need to watch out for member fields that are uppercased by 80 | // the user. For example (URL, ID, ...) 81 | if !isUppercase(fieldName) { 82 | fieldName = string(unicode.ToUpper(rune(fieldName[0]))) + fieldName[1:] 83 | } 84 | 85 | fieldValue := getFieldAndTagByName(data, fieldName) 86 | for _, set := range ruleSets { 87 | set.FieldValue = fieldValue 88 | set.FieldName = fieldName 89 | fieldName = string(unicode.ToLower([]rune(fieldName)[0])) + fieldName[1:] 90 | if !set.ValidateFunc(set) { 91 | ok = false 92 | msg := set.MessageFunc(set) 93 | if len(set.ErrorMessage) > 0 { 94 | msg = set.ErrorMessage 95 | } 96 | if _, ok := errors[fieldName]; !ok { 97 | errors[fieldName] = []string{} 98 | } 99 | errors[fieldName] = append(errors[fieldName], msg) 100 | } 101 | } 102 | } 103 | return errors, ok 104 | } 105 | 106 | func getFieldAndTagByName(v any, name string) any { 107 | val := reflect.ValueOf(v) 108 | if val.Kind() == reflect.Ptr { 109 | val = val.Elem() 110 | } 111 | if val.Kind() != reflect.Struct { 112 | return nil 113 | } 114 | fieldVal := val.FieldByName(name) 115 | if !fieldVal.IsValid() { 116 | return nil 117 | } 118 | return fieldVal.Interface() 119 | } 120 | 121 | func parseRequest(r *http.Request, v any) error { 122 | contentType := r.Header.Get("Content-Type") 123 | if contentType == "application/x-www-form-urlencoded" { 124 | if err := r.ParseForm(); err != nil { 125 | return fmt.Errorf("failed to parse form: %v", err) 126 | } 127 | val := reflect.ValueOf(v).Elem() 128 | for i := 0; i < val.NumField(); i++ { 129 | field := val.Type().Field(i) 130 | formTag := field.Tag.Get("form") 131 | formValue := r.FormValue(formTag) 132 | 133 | if formValue == "" { 134 | continue 135 | } 136 | 137 | fieldVal := val.Field(i) 138 | switch fieldVal.Kind() { 139 | case reflect.Bool: 140 | // There are cases where frontend libraries use "on" as the bool value 141 | // think about toggles. Hence, let's try this first. 142 | if formValue == "on" { 143 | fieldVal.SetBool(true) 144 | } else if formValue == "off" { 145 | fieldVal.SetBool(false) 146 | return nil 147 | } else { 148 | boolVal, err := strconv.ParseBool(formValue) 149 | if err != nil { 150 | return fmt.Errorf("failed to parse bool: %v", err) 151 | } 152 | fieldVal.SetBool(boolVal) 153 | } 154 | case reflect.String: 155 | fieldVal.SetString(formValue) 156 | case reflect.Int, reflect.Int32, reflect.Int64: 157 | intVal, err := strconv.Atoi(formValue) 158 | if err != nil { 159 | return fmt.Errorf("failed to parse int: %v", err) 160 | } 161 | fieldVal.SetInt(int64(intVal)) 162 | case reflect.Uint, reflect.Uint32, reflect.Uint64: 163 | intVal, err := strconv.Atoi(formValue) 164 | if err != nil { 165 | return fmt.Errorf("failed to parse int: %v", err) 166 | } 167 | fieldVal.SetUint(uint64(intVal)) 168 | case reflect.Float64: 169 | floatVal, err := strconv.ParseFloat(formValue, 64) 170 | if err != nil { 171 | return fmt.Errorf("failed to parse float: %v", err) 172 | } 173 | fieldVal.SetFloat(floatVal) 174 | default: 175 | return fmt.Errorf("unsupported kind %s", fieldVal.Kind()) 176 | } 177 | } 178 | 179 | } 180 | return nil 181 | } 182 | 183 | func isUppercase(s string) bool { 184 | for _, ch := range s { 185 | if !unicode.IsUpper(rune(ch)) { 186 | return false 187 | } 188 | } 189 | return true 190 | } 191 | -------------------------------------------------------------------------------- /validate/validate_test.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var createdAt = time.Now() 15 | 16 | var testSchema = Schema{ 17 | "createdAt": Rules(Time), 18 | "startedAt": Rules(TimeBefore(time.Now())), 19 | "deletedAt": Rules(TimeAfter(createdAt)), 20 | "email": Rules(Email), 21 | "url": Rules(URL), 22 | "password": Rules( 23 | ContainsSpecial, 24 | ContainsUpper, 25 | ContainsDigit, 26 | Min(7), 27 | Max(50), 28 | ), 29 | "age": Rules(GTE(18)), 30 | "bet": Rules(GT(0), LTE(10)), 31 | "username": Rules(Required), 32 | } 33 | 34 | func TestValidateRequest(t *testing.T) { 35 | var ( 36 | email = "foo@bar.com" 37 | password = "superHunter123@" 38 | firstName = "Anthony" 39 | website = "http://foo.com" 40 | randomNumber = 123 41 | randomFloat = 9.999 42 | ) 43 | formValues := url.Values{} 44 | formValues.Set("email", email) 45 | formValues.Set("password", password) 46 | formValues.Set("firstName", firstName) 47 | formValues.Set("url", website) 48 | formValues.Set("brandom", fmt.Sprint(randomNumber)) 49 | formValues.Set("arandom", fmt.Sprint(randomFloat)) 50 | encodedValues := formValues.Encode() 51 | 52 | req, err := http.NewRequest("POST", "http://foo.com", strings.NewReader(encodedValues)) 53 | assert.Nil(t, err) 54 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 55 | 56 | type SignupData struct { 57 | Email string `form:"email"` 58 | Password string `form:"password"` 59 | FirstName string `form:"firstName"` 60 | URL string `form:"url"` 61 | ARandomRenamedNumber int `form:"brandom"` 62 | ARandomRenamedFloat float64 `form:"arandom"` 63 | } 64 | 65 | schema := Schema{ 66 | "Email": Rules(Email), 67 | "Password": Rules( 68 | Required, 69 | ContainsDigit, 70 | ContainsUpper, 71 | ContainsSpecial, 72 | Min(7), 73 | ), 74 | "FirstName": Rules(Min(3), Max(50)), 75 | "URL": Rules(URL), 76 | "ARandomRenamedNumber": Rules(GT(100), LT(124)), 77 | "ARandomRenamedFloat": Rules(GT(9.0), LT(10.1)), 78 | } 79 | 80 | var data SignupData 81 | errors, ok := Request(req, &data, schema) 82 | assert.True(t, ok) 83 | assert.Empty(t, errors) 84 | 85 | assert.Equal(t, data.Email, email) 86 | assert.Equal(t, data.Password, password) 87 | assert.Equal(t, data.FirstName, firstName) 88 | assert.Equal(t, data.URL, website) 89 | assert.Equal(t, data.ARandomRenamedNumber, randomNumber) 90 | assert.Equal(t, data.ARandomRenamedFloat, randomFloat) 91 | } 92 | 93 | func TestTime(t *testing.T) { 94 | type Foo struct { 95 | CreatedAt time.Time 96 | } 97 | foo := Foo{ 98 | CreatedAt: time.Now(), 99 | } 100 | schema := Schema{ 101 | "createdAt": Rules(Time), 102 | } 103 | _, ok := Validate(foo, schema) 104 | assert.True(t, ok) 105 | 106 | foo.CreatedAt = time.Time{} 107 | _, ok = Validate(foo, schema) 108 | assert.False(t, ok) 109 | } 110 | 111 | func TestURL(t *testing.T) { 112 | type Foo struct { 113 | URL string `v:"URL"` 114 | } 115 | foo := Foo{ 116 | URL: "not an url", 117 | } 118 | schema := Schema{ 119 | "URL": Rules(URL), 120 | } 121 | errors, ok := Validate(foo, schema) 122 | assert.False(t, ok) 123 | assert.NotEmpty(t, errors) 124 | 125 | validURLS := []string{ 126 | "http://google.com", 127 | "http://www.google.com", 128 | "https://www.google.com", 129 | "https://www.google.com", 130 | "www.google.com", 131 | "https://book.com/sales", 132 | "app.book.com", 133 | "app.book.com/signup", 134 | } 135 | 136 | for _, url := range validURLS { 137 | foo.URL = url 138 | errors, ok = Validate(foo, schema) 139 | assert.True(t, ok) 140 | assert.Empty(t, errors) 141 | } 142 | } 143 | 144 | func TestContainsUpper(t *testing.T) { 145 | type Foo struct { 146 | Password string 147 | } 148 | foo := Foo{"hunter"} 149 | schema := Schema{ 150 | "Password": Rules(ContainsUpper), 151 | } 152 | errors, ok := Validate(foo, schema) 153 | assert.False(t, ok) 154 | assert.NotEmpty(t, errors) 155 | 156 | foo.Password = "Hunter" 157 | errors, ok = Validate(foo, schema) 158 | assert.True(t, ok) 159 | assert.Empty(t, errors) 160 | } 161 | 162 | func TestContainsDigit(t *testing.T) { 163 | type Foo struct { 164 | Password string 165 | } 166 | foo := Foo{"hunter"} 167 | schema := Schema{ 168 | "Password": Rules(ContainsDigit), 169 | } 170 | errors, ok := Validate(foo, schema) 171 | assert.False(t, ok) 172 | assert.NotEmpty(t, errors) 173 | 174 | foo.Password = "Hunter1" 175 | errors, ok = Validate(foo, schema) 176 | assert.True(t, ok) 177 | assert.Empty(t, errors) 178 | } 179 | 180 | func TestContainsSpecial(t *testing.T) { 181 | type Foo struct { 182 | Password string 183 | } 184 | foo := Foo{"hunter"} 185 | schema := Schema{ 186 | "Password": Rules(ContainsSpecial), 187 | } 188 | errors, ok := Validate(foo, schema) 189 | assert.False(t, ok) 190 | assert.NotEmpty(t, errors) 191 | 192 | foo.Password = "Hunter@" 193 | errors, ok = Validate(foo, schema) 194 | assert.True(t, ok) 195 | assert.Empty(t, errors) 196 | } 197 | 198 | func TestRuleIn(t *testing.T) { 199 | type Foo struct { 200 | Currency string 201 | } 202 | foo := Foo{"eur"} 203 | schema := Schema{ 204 | "currency": Rules(In([]string{"eur", "usd", "chz"})), 205 | } 206 | errors, ok := Validate(foo, schema) 207 | assert.True(t, ok) 208 | assert.Empty(t, errors) 209 | foo = Foo{"foo"} 210 | errors, ok = Validate(foo, schema) 211 | assert.False(t, ok) 212 | assert.Len(t, errors["currency"], 1) 213 | } 214 | 215 | func TestValidate(t *testing.T) { 216 | type User struct { 217 | Email string 218 | Username string 219 | } 220 | schema := Schema{ 221 | "email": Rules(Email), 222 | // Test both lower and uppercase 223 | "Username": Rules(Min(3), Max(10)), 224 | } 225 | user := User{ 226 | Email: "foo@bar.com", 227 | Username: "pedropedro", 228 | } 229 | errors, ok := Validate(user, schema) 230 | assert.True(t, ok) 231 | assert.Empty(t, errors) 232 | } 233 | 234 | func TestMergeSchemas(t *testing.T) { 235 | expected := Schema{ 236 | "Name": Rules(), 237 | "Email": Rules(), 238 | "FirstName": Rules(), 239 | "LastName": Rules(), 240 | } 241 | a := Schema{ 242 | "Name": Rules(), 243 | "Email": Rules(), 244 | } 245 | b := Schema{ 246 | "FirstName": Rules(), 247 | "LastName": Rules(), 248 | } 249 | c := Merge(a, b) 250 | assert.Equal(t, expected, c) 251 | } 252 | -------------------------------------------------------------------------------- /view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/anthdm/superkit/kit" 10 | "github.com/anthdm/superkit/kit/middleware" 11 | ) 12 | 13 | // Asset is a view helper that returns the full asset path as a 14 | // string based on the given asset name. 15 | // 16 | // view.Asset("styles.css") // => /public/assets/styles.css. 17 | func Asset(name string) string { 18 | return fmt.Sprintf("/public/assets/%s", name) 19 | } 20 | 21 | // getContextValue is a helper function to retrieve a value from the context. 22 | // It returns the value if present, otherwise returns the provided default value. 23 | func getContextValue[T any](ctx context.Context, key interface{}, defaultValue T) T { 24 | value, ok := ctx.Value(key).(T) 25 | if !ok { 26 | return defaultValue 27 | } 28 | return value 29 | } 30 | 31 | // Auth is a view helper function that returns the current Auth. 32 | // If Auth is not set, a default Auth will be returned. 33 | // 34 | // view.Auth(ctx) 35 | func Auth(ctx context.Context) kit.Auth { 36 | return getContextValue(ctx, kit.AuthKey{}, kit.DefaultAuth{}) 37 | } 38 | 39 | // URL is a view helper that returns the current URL. 40 | // The request path can be accessed with: 41 | // 42 | // view.URL(ctx).Path // => ex. /login 43 | func URL(ctx context.Context) *url.URL { 44 | return getContextValue(ctx, middleware.RequestKey{}, &http.Request{}).URL 45 | } 46 | 47 | // Request is a view helper that returns the current http request. 48 | // The request can be accessed with: 49 | // 50 | // view.Request(ctx) 51 | func Request(ctx context.Context) *http.Request { 52 | return getContextValue(ctx, middleware.RequestKey{}, &http.Request{}) 53 | } 54 | --------------------------------------------------------------------------------