├── .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 |
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 |
34 | }
35 | }
36 |
37 | templ LoginForm(values LoginFormValues, errors v.Errors) {
38 |
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 |
20 |
21 | @ProfileForm(formValues, v.Errors{})
22 |
23 | }
24 | }
25 |
26 | templ ProfileForm(values ProfileFormValues, errors v.Errors) {
27 |
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 |
94 | }
95 |
96 | templ ConfirmEmail(user User) {
97 |
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 |
13 |
14 |
25 |
36 | { children... }
37 |
38 | foo
39 |
40 |
41 |
42 |
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 |
--------------------------------------------------------------------------------