Compare commits

..

1 commit

Author SHA1 Message Date
anthdm
4cf5175548 wip: ui components 2024-06-22 15:19:04 +02:00
39 changed files with 1224 additions and 351 deletions

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 @anthdm and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,2 +0,0 @@
test:
@go test -v ./...

View file

@ -6,23 +6,21 @@ endif
ifeq ($(OS),Windows_NT)
MAIN_PATH = /tmp/bin/main.exe
SYNC_ASSETS_COMMAND = @go run github.com/makiuchi-d/arelo@v1.13.1 \
SYNC_ASSETS_COMMAND = go run github.com/makiuchi-d/arelo@v1.13.1 \
--target "./public" \
--pattern "**/*.js" \
--pattern "**/*.css" \
--delay "100ms" \
--templ generate --notify-proxy
-- templ generate --notify-proxy
else
MAIN_PATH = tmp/bin/main
SYNC_ASSETS_COMMAND = @go run github.com/cosmtrek/air@v1.51.0 \
SYNC_ASSETS_COMMAND = go run github.com/cosmtrek/air@v1.51.0 \
--build.cmd "templ generate --notify-proxy" \
--build.bin "true" \
--build.delay "100" \
--build.exclude_dir "" \
--build.include_dir "public" \
--build.include_ext "js,css" \
--screen.clear_on_rebuild true \
--log.main_only true
--build.include_ext "js,css"
endif
# run templ generation in watch mode to detect all .templ files and
@ -38,17 +36,15 @@ server:
--build.exclude_dir "node_modules" \
--build.include_ext "go" \
--build.stop_on_error "false" \
--misc.clean_on_exit true \
--screen.clear_on_rebuild true \
--log.main_only true
--misc.clean_on_exit true
# run tailwindcss to generate the styles.css bundle in watch mode.
watch-assets:
@npx tailwindcss -i app/assets/app.css -o ./public/assets/styles.css --watch
npx tailwindcss -i app/assets/app.css -o ./public/assets/styles.css --watch
# run esbuild to generate the index.js bundle in watch mode.
watch-esbuild:
@npx esbuild app/assets/index.js --bundle --outdir=public/assets --watch
npx esbuild app/assets/index.js --bundle --outdir=public/assets --watch
# watch for any js or css change in the assets/ folder, then reload the browser via templ proxy.
sync_assets:
@ -67,7 +63,7 @@ build:
@echo "compiled you application with all its assets to a single binary => bin/app_prod"
db-status:
@GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) go run github.com/pressly/goose/v3/cmd/goose@latest -dir=$(MIGRATION_DIR) status
@GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) go run github.com/pressly/goose/v3/cmd/goose@latest status
db-reset:
@GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) go run github.com/pressly/goose/v3/cmd/goose@latest -dir=$(MIGRATION_DIR) reset
@ -82,4 +78,4 @@ db-mig-create:
@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
db-seed:
@go run cmd/scripts/seed/main.go
@go run cmd/scripts/seed/main.go

View file

@ -1,3 +1,3 @@
package conf
// Here goes your application config
// Application config

View file

@ -5,25 +5,21 @@ import (
"os"
"github.com/anthdm/superkit/db"
"github.com/anthdm/superkit/kit"
_ "github.com/mattn/go-sqlite3"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/extra/bundebug"
)
// By default this is a pre-configured Gorm DB instance.
// Change this type based on the database package of your likings.
var dbInstance *gorm.DB
// Get returns the instantiated DB instance.
func Get() *gorm.DB {
return dbInstance
}
// I could not came up with a better naming for this.
// Ideally, app should export a global variable called "DB"
// but this will cause imports cycles for plugins.
var Query *bun.DB
func init() {
// Create a default *sql.DB exposed by the superkit/db package
// based on the given configuration.
config := db.Config{
Driver: os.Getenv("DB_DRIVER"),
Name: os.Getenv("DB_NAME"),
@ -31,30 +27,12 @@ func init() {
User: os.Getenv("DB_USER"),
Host: os.Getenv("DB_HOST"),
}
dbinst, err := db.NewSQL(config)
db, err := db.New(config)
if err != nil {
log.Fatal(err)
}
// Based on the driver create the corresponding DB instance.
// By default, the SuperKit boilerplate comes with a pre-configured
// ORM called Gorm. https://gorm.io.
//
// You can change this to any other DB interaction tool
// of your liking. EG:
// - uptrace bun -> https://bun.uptrace.dev
// - SQLC -> https://github.com/sqlc-dev/sqlc
// - gojet -> https://github.com/go-jet/jet
switch config.Driver {
case db.DriverSqlite3:
dbInstance, err = gorm.Open(sqlite.New(sqlite.Config{
Conn: dbinst,
}))
case db.DriverMysql:
// ...
default:
log.Fatal("invalid driver:", config.Driver)
}
if err != nil {
log.Fatal(err)
Query = bun.NewDB(db, sqlitedialect.New())
if kit.IsDevelopment() {
Query.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
}
}

View file

View file

@ -5,10 +5,9 @@ create table if not exists users(
password_hash text not null,
first_name text not null,
last_name text not null,
email_verified_at datetime,
created_at datetime not null,
updated_at datetime not null,
deleted_at datetime
email_verified_at timestamp with time zone,
created_at timestamp with time zone not null,
updated_at timestamp with time zone not null
);
-- +goose Down

View file

@ -5,10 +5,9 @@ create table if not exists sessions(
user_id integer not null references users,
ip_address text,
user_agent text,
expires_at datetime not null,
created_at datetime not null,
updated_at datetime not null,
deleted_at datetime
expires_at timestamp with time zone not null,
last_login_at timestamp with time zone,
created_at timestamp with time zone not null
);
-- +goose Down

View file

@ -0,0 +1,11 @@
package handlers
import (
"AABBCCDD/app/views/showcase"
"github.com/anthdm/superkit/kit"
)
func HandleShowcaseIndex(kit *kit.Kit) error {
return kit.Render(showcase.Index())
}

View file

@ -17,14 +17,14 @@ import (
func InitializeMiddleware(router *chi.Mux) {
router.Use(chimiddleware.Logger)
router.Use(chimiddleware.Recoverer)
router.Use(middleware.WithRequest)
router.Use(middleware.WithRequestURL)
}
// Define your routes in here
func InitializeRoutes(router *chi.Mux) {
// Authentication plugin
//
// By default the auth plugin is active, to disable the auth plugin
// By default the auth plugin is active. To disable the auth plugin
// you will need to pass your own handler in the `AuthFunc`` field
// of the `kit.AuthenticationConfig`.
// authConfig := kit.AuthenticationConfig{

View file

@ -2,7 +2,7 @@ package types
// AuthUser represents an user that might be authenticated.
type AuthUser struct {
ID uint
ID int
Email string
LoggedIn bool
}

View file

@ -0,0 +1,157 @@
package showcase
import (
"AABBCCDD/app/views/layouts"
"AABBCCDD/ui/button"
"AABBCCDD/ui/input"
"AABBCCDD/ui/modal"
"AABBCCDD/ui/card"
"AABBCCDD/ui"
"AABBCCDD/ui/table"
)
templ Index() {
@layouts.App() {
<div class="flex flex-col gap-10 mt-10 py-8">
<div>
<h1 class="text-2xl font-semibold">Buttons</h1>
<div class="border-b pt-2 mb-6"></div>
<div class="flex gap-4">
<button { button.Primary()... }>primary</button>
<button { button.Outline()... }>outline</button>
<button { button.Secondary()... }>secondary</button>
<button { button.Destructive()... }>destructive</button>
</div>
</div>
<div>
<h1 class="text-2xl font-semibold">Forms</h1>
<div class="border-b pt-2 mb-6"></div>
<form class="flex flex-col gap-4 w-full max-w-xs">
<div class="flex flex-col gap-2">
<label class="text-sm">Email</label>
<input name="email" type="email" placeholder="enter your email" { input.Input()... }/>
</div>
<div class="flex flex-col gap-2">
<label class="text-sm">First Name</label>
<input name="firstName" type="text" placeholder="first name" { input.Input()... }/>
</div>
<div class="flex flex-col gap-2">
<label class="text-sm">Last Name</label>
<input name="lastName" type="text" placeholder="last name" { input.Input()... }/>
</div>
<button type="button" { button.Primary(ui.Class("w-fit"))... }>submit</button>
</form>
</div>
<div>
<h1 class="text-2xl font-semibold">Card</h1>
<div class="border-b pt-2 mb-6"></div>
@card.Card(ui.Class("max-w-md")) {
@card.Header() {
<h1 class="text-xl">User registration</h1>
<h2 class="text-muted-foreground">Please register to get the latest updates</h2>
}
@card.Content() {
<form class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="text-sm">Username</label>
<input { input.Input()... }/>
</div>
<div class="flex flex-col gap-1">
<label class="text-sm">Email</label>
<input { input.Input()... }/>
</div>
</form>
}
@card.Footer() {
<button { button.Primary()... }>Submit</button>
}
}
</div>
<div>
<h1 class="text-2xl font-semibold">Table</h1>
<div class="border-b pt-2 mb-6"></div>
<div class="max-w-4xl">
@table.Table() {
@table.Header(ui.Class("text-left")) {
<th { table.Th()... }>
<div class="flex items-center">
<input id="checkbox-id" type="checkbox" class="w-4 h-4 bg-transparent border-input rounded"/>
</div>
</th>
<th { table.Th()... }>id</th>
<th { table.Th()... }>email</th>
<th { table.Th()... }>first name</th>
<th { table.Th()... }>last name</th>
<th { table.Th(ui.Class("text-right"))... }>action</th>
}
@table.Body() {
<tr>
<td { table.Td()... }>
<div class="flex items-center">
<input id="checkbox-id" type="checkbox" class="w-4 h-4 bg-transparent border-input rounded"/>
</div>
</td>
<td { table.Td()... }>1</td>
<td { table.Td()... }>foo@foo.com</td>
<td { table.Td()... }>Anthony</td>
<td { table.Td()... }>GG</td>
<td { table.Td(ui.Class("text-right text-blue-500"))... }>view</td>
</tr>
<tr>
<td { table.Td()... }>
<div class="flex items-center">
<input id="checkbox-id" type="checkbox" class="w-4 h-4 bg-transparent border-input rounded"/>
</div>
</td>
<td { table.Td()... }>1</td>
<td { table.Td()... }>foo@foo.com</td>
<td { table.Td()... }>Anthony</td>
<td { table.Td()... }>GG</td>
<td { table.Td(ui.Class("text-right text-blue-500"))... }>view</td>
</tr>
<tr>
<td { table.Td()... }>
<div class="flex items-center">
<input id="checkbox-id" type="checkbox" class="w-4 h-4 bg-transparent border-input rounded"/>
</div>
</td>
<td { table.Td()... }>1</td>
<td { table.Td()... }>foo@foo.com</td>
<td { table.Td()... }>Anthony</td>
<td { table.Td()... }>GG</td>
<td { table.Td(ui.Class("text-right text-blue-500"))... }>view</td>
</tr>
<tr>
<td { table.Td()... }>
<div class="flex items-center">
<input id="checkbox-id" type="checkbox" class="w-4 h-4 bg-transparent border-input rounded"/>
</div>
</td>
<td { table.Td()... }>1</td>
<td { table.Td()... }>foo@foo.com</td>
<td { table.Td()... }>Anthony</td>
<td { table.Td()... }>GG</td>
<td { table.Td(ui.Class("text-right text-blue-500"))... }>view</td>
</tr>
}
}
</div>
</div>
<div>
<h1 class="text-2xl font-semibold">Modal</h1>
<div class="border-b pt-2 mb-6"></div>
@modal.Modal() {
@modal.Header() {
the header
}
@modal.Content() {
the content
}
@modal.Trigger() {
<div class="text-white">trigger me</div>
}
}
</div>
</div>
}
}

View file

@ -14,7 +14,10 @@ import (
)
func main() {
kit.Setup()
if err := godotenv.Load(); err != nil {
log.Fatal(err)
}
router := chi.NewMux()
app.InitializeMiddleware(router)

View file

@ -6,21 +6,28 @@ go 1.22.4
// replace github.com/anthdm/superkit => ../
require (
github.com/a-h/templ v0.2.731
github.com/anthdm/superkit v0.0.0-20240622052611-30be5bb82e0d
github.com/go-chi/chi/v5 v5.0.14
github.com/a-h/templ v0.2.707
github.com/anthdm/superkit v0.0.0-20240616155928-19996932bf4f
github.com/go-chi/chi/v5 v5.0.12
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.22
github.com/uptrace/bun v1.2.1
github.com/uptrace/bun/dialect/sqlitedialect v1.2.1
github.com/uptrace/bun/extra/bundebug v1.2.1
golang.org/x/crypto v0.24.0
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.10
)
require (
github.com/fatih/color v1.16.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/sys v0.21.0 // indirect
)

View file

@ -1,9 +1,11 @@
github.com/a-h/templ v0.2.731 h1:yiv4C7whSUsa36y65O06DPr/U/j3+WGB0RmvLOoVFXc=
github.com/a-h/templ v0.2.731/go.mod h1:IejA/ecDD0ul0dCvgCwp9t7bUZXVpGClEAdsqZQigi8=
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
@ -18,21 +20,36 @@ github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFz
github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.2.1 h1:2ENAcfeCfaY5+2e7z5pXrzFKy3vS8VXvkCag6N2Yzfk=
github.com/uptrace/bun v1.2.1/go.mod h1:cNg+pWBUMmJ8rHnETgf65CEvn3aIKErrwOD6IA8e+Ec=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.1 h1:IprvkIKUjEjvt4VKpcmLpbMIucjrsmUPJOSlg19+a0Q=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.1/go.mod h1:mMQf4NUpgY8bnOanxGmxNiHCdALOggS4cZ3v63a9D/o=
github.com/uptrace/bun/extra/bundebug v1.2.1 h1:85MYpX3QESYI02YerKxUi1CD9mHuLrc2BXs1eOCtQus=
github.com/uptrace/bun/extra/bundebug v1.2.1/go.mod h1:sfGKIi0HSGxsTC/sgIHGwpnYduHHYhdMeOIwurgSY+Y=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

View file

@ -1 +0,0 @@
Add your packages aka custom libraries here.

View file

@ -13,7 +13,6 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
const (
@ -41,9 +40,12 @@ func HandleLoginCreate(kit *kit.Kit) error {
}
var user User
err := db.Get().Find(&user, "email = ?", values.Email).Error
err := db.Query.NewSelect().
Model(&user).
Where("user.email = ?", values.Email).
Scan(kit.Request.Context())
if err != nil {
if err == gorm.ErrRecordNotFound {
if err == sql.ErrNoRows {
errors.Add("credentials", "invalid credentials")
return kit.Render(LoginForm(values, errors))
}
@ -57,7 +59,7 @@ func HandleLoginCreate(kit *kit.Kit) error {
skipVerify := kit.Getenv("SUPERKIT_AUTH_SKIP_VERIFY", "false")
if skipVerify != "true" {
if !user.EmailVerifiedAt.Valid {
if user.EmailVerifiedAt.Equal(time.Time{}) {
errors.Add("verified", "please verify your email")
return kit.Render(LoginForm(values, errors))
}
@ -69,17 +71,24 @@ func HandleLoginCreate(kit *kit.Kit) error {
sessionExpiry = 48
}
session := Session{
UserID: user.ID,
Token: uuid.New().String(),
ExpiresAt: time.Now().Add(time.Hour * time.Duration(sessionExpiry)),
UserID: user.ID,
Token: uuid.New().String(),
CreatedAt: time.Now(),
LastLoginAt: time.Now(),
ExpiresAt: time.Now().Add(time.Hour * time.Duration(sessionExpiry)),
}
if err = db.Get().Create(&session).Error; err != nil {
_, err = db.Query.NewInsert().
Model(&session).
Exec(kit.Request.Context())
if err != nil {
return err
}
// TODO change this with kit.Getenv
sess := kit.GetSession(userSessionName)
sess.Values["sessionToken"] = session.Token
sess.Save(kit.Request, kit.Response)
redirectURL := kit.Getenv("SUPERKIT_AUTH_REDIRECT_AFTER_LOGIN", "/profile")
return kit.Redirect(http.StatusSeeOther, redirectURL)
@ -91,7 +100,10 @@ func HandleLoginDelete(kit *kit.Kit) error {
sess.Values = map[any]any{}
sess.Save(kit.Request, kit.Response)
}()
err := db.Get().Delete(&Session{}, "token = ?", sess.Values["sessionToken"]).Error
_, err := db.Query.NewDelete().
Model((*Session)(nil)).
Where("token = ?", sess.Values["sessionToken"]).
Exec(kit.Request.Context())
if err != nil {
return err
}
@ -109,7 +121,7 @@ func HandleEmailVerify(kit *kit.Kit) error {
return []byte(os.Getenv("SUPERKIT_SECRET")), nil
}, jwt.WithLeeway(5*time.Second))
if err != nil {
return kit.Render(EmailVerificationError("invalid verification token"))
return err
}
if !token.Valid {
return kit.Render(EmailVerificationError("invalid verification token"))
@ -129,18 +141,23 @@ func HandleEmailVerify(kit *kit.Kit) error {
}
var user User
err = db.Get().First(&user, userID).Error
err = db.Query.NewSelect().
Model(&user).
Where("id = ?", userID).
Scan(kit.Request.Context())
if err != nil {
return err
}
if user.EmailVerifiedAt.Time.After(time.Time{}) {
if user.EmailVerifiedAt.After(time.Time{}) {
return kit.Render(EmailVerificationError("Email already verified"))
}
now := sql.NullTime{Time: time.Now(), Valid: true}
user.EmailVerifiedAt = now
err = db.Get().Save(&user).Error
user.EmailVerifiedAt = time.Now()
_, err = db.Query.NewUpdate().
Model(&user).
WherePK().
Exec(kit.Request.Context())
if err != nil {
return err
}
@ -157,14 +174,16 @@ func AuthenticateUser(kit *kit.Kit) (kit.Auth, error) {
}
var session Session
err := db.Get().
Preload("User").
Find(&session, "token = ? AND expires_at > ?", token, time.Now()).Error
if err != nil || session.ID == 0 {
err := db.Query.NewSelect().
Model(&session).
Relation("User").
Where("session.token = ? AND session.expires_at > ?", token, time.Now()).
Scan(kit.Request.Context())
if err != nil {
return auth, nil
}
return Auth{
LoggedIn: true,
UserID: session.User.ID,
Email: session.User.Email,

View file

@ -14,7 +14,7 @@ var profileSchema = v.Schema{
}
type ProfileFormValues struct {
ID uint `form:"id"`
ID int `form:"id"`
FirstName string `form:"firstName"`
LastName string `form:"lastName"`
Email string
@ -25,7 +25,11 @@ func HandleProfileShow(kit *kit.Kit) error {
auth := kit.Auth().(Auth)
var user User
if err := db.Get().First(&user, auth.UserID).Error; err != nil {
err := db.Query.NewSelect().
Model(&user).
Where("id = ?", auth.UserID).
Scan(kit.Request.Context())
if err != nil {
return err
}
@ -50,12 +54,12 @@ func HandleProfileUpdate(kit *kit.Kit) error {
if auth.UserID != values.ID {
return fmt.Errorf("unauthorized request for profile %d", values.ID)
}
err := db.Get().Model(&User{}).
_, err := db.Query.NewUpdate().
Model((*User)(nil)).
Set("first_name = ?", values.FirstName).
Set("last_name = ?", values.LastName).
Where("id = ?", auth.UserID).
Updates(&User{
FirstName: values.FirstName,
LastName: values.LastName,
}).Error
Exec(kit.Request.Context())
if err != nil {
return err
}

View file

@ -63,15 +63,19 @@ func HandleResendVerificationCode(kit *kit.Kit) error {
}
var user User
if err = db.Get().First(&user, id).Error; err != nil {
err = db.Query.NewSelect().
Model(&user).
Where("id = ?", id).
Scan(kit.Request.Context())
if err != nil {
return kit.Text(http.StatusOK, "An unexpected error occured")
}
if user.EmailVerifiedAt.Time.After(time.Time{}) {
if user.EmailVerifiedAt.After(time.Time{}) {
return kit.Text(http.StatusOK, "Email already verified!")
}
token, err := createVerificationToken(uint(id))
token, err := createVerificationToken(id)
if err != nil {
return kit.Text(http.StatusOK, "An unexpected error occured")
}
@ -86,7 +90,7 @@ func HandleResendVerificationCode(kit *kit.Kit) error {
return kit.Text(http.StatusOK, msg)
}
func createVerificationToken(userID uint) (string, error) {
func createVerificationToken(userID int) (string, error) {
expiryStr := kit.Getenv("SUPERKIT_AUTH_EMAIL_VERIFICATION_EXPIRY_IN_HOURS", "1")
expiry, err := strconv.Atoi(expiryStr)
if err != nil {

View file

@ -2,11 +2,10 @@ package auth
import (
"AABBCCDD/app/db"
"database/sql"
"context"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// Event name constants
@ -23,7 +22,7 @@ type UserWithVerificationToken struct {
}
type Auth struct {
UserID uint
UserID int
Email string
LoggedIn bool
}
@ -33,13 +32,12 @@ func (auth Auth) Check() bool {
}
type User struct {
gorm.Model
ID int `bun:"id,pk,autoincrement"`
Email string
FirstName string
LastName string
PasswordHash string
EmailVerifiedAt sql.NullTime
EmailVerifiedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
@ -54,19 +52,22 @@ func createUserFromFormValues(values SignupFormValues) (User, error) {
FirstName: values.FirstName,
LastName: values.LastName,
PasswordHash: string(hash),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
result := db.Get().Create(&user)
return user, result.Error
_, err = db.Query.NewInsert().Model(&user).Exec(context.Background())
return user, err
}
type Session struct {
gorm.Model
ID int `bun:"id,pk,autoincrement"`
UserID int
Token string
IPAddress string
UserAgent string
ExpiresAt time.Time
LastLoginAt time.Time
CreatedAt time.Time
UserID uint
Token string
IPAddress string
UserAgent string
ExpiresAt time.Time
CreatedAt time.Time
User User
User User `bun:"rel:belongs-to,join:user_id=id"`
}

View file

@ -629,23 +629,63 @@ body {
position: fixed;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-0 {
inset: 0px;
}
.left-0 {
left: 0px;
}
.right-0 {
right: 0px;
}
.right-6 {
right: 1.5rem;
}
.top-0 {
top: 0px;
}
.top-6 {
top: 1.5rem;
}
.z-50 {
z-index: 50;
}
.z-\[99\] {
z-index: 99;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.ml-4 {
margin-left: 1rem;
}
.mr-5 {
margin-right: 1.25rem;
}
.mt-10 {
margin-top: 2.5rem;
}
@ -654,6 +694,14 @@ body {
margin-top: 8rem;
}
.mt-5 {
margin-top: 1.25rem;
}
.mt-8 {
margin-top: 2rem;
}
.inline-block {
display: inline-block;
}
@ -666,14 +714,58 @@ body {
display: inline-flex;
}
.table {
display: table;
}
.hidden {
display: none;
}
.h-10 {
height: 2.5rem;
}
.h-4 {
height: 1rem;
}
.h-5 {
height: 1.25rem;
}
.h-8 {
height: 2rem;
}
.h-auto {
height: auto;
}
.h-full {
height: 100%;
}
.h-screen {
height: 100vh;
}
.w-4 {
width: 1rem;
}
.w-5 {
width: 1.25rem;
}
.w-8 {
width: 2rem;
}
.w-auto {
width: auto;
}
.w-fit {
width: -moz-fit-content;
width: fit-content;
@ -683,10 +775,22 @@ body {
width: 100%;
}
.w-screen {
width: 100vw;
}
.min-w-full {
min-width: 100%;
}
.max-w-2xl {
max-width: 42rem;
}
.max-w-4xl {
max-width: 56rem;
}
.max-w-7xl {
max-width: 80rem;
}
@ -699,6 +803,20 @@ body {
max-width: 24rem;
}
.max-w-xs {
max-width: 20rem;
}
.translate-y-0 {
--tw-translate-y: 0px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-y-4 {
--tw-translate-y: 1rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.cursor-pointer {
cursor: pointer;
}
@ -755,6 +873,36 @@ body {
gap: 2rem;
}
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
}
.overflow-hidden {
overflow: hidden;
}
.overflow-x-auto {
overflow-x: auto;
}
.whitespace-nowrap {
white-space: nowrap;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-full {
border-radius: 9999px;
}
.rounded-lg {
border-radius: var(--radius);
}
.rounded-md {
border-radius: calc(var(--radius) - 2px);
}
@ -772,20 +920,49 @@ body {
border-color: hsl(var(--input) / var(--tw-border-opacity));
}
.border-primary {
--tw-border-opacity: 1;
border-color: hsl(var(--primary) / var(--tw-border-opacity));
}
.border-red-500 {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.bg-black {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
.bg-card {
--tw-bg-opacity: 1;
background-color: hsl(var(--card) / var(--tw-bg-opacity));
}
.bg-destructive {
--tw-bg-opacity: 1;
background-color: hsl(var(--destructive) / var(--tw-bg-opacity));
}
.bg-primary {
--tw-bg-opacity: 1;
background-color: hsl(var(--primary) / var(--tw-bg-opacity));
}
.bg-secondary {
--tw-bg-opacity: 1;
background-color: hsl(var(--secondary) / var(--tw-bg-opacity));
}
.bg-transparent {
background-color: transparent;
}
.bg-opacity-40 {
--tw-bg-opacity: 0.4;
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
@ -810,6 +987,10 @@ body {
background-clip: text;
}
.p-6 {
padding: 1.5rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
@ -820,16 +1001,31 @@ body {
padding-right: 1rem;
}
.px-5 {
padding-left: 1.25rem;
padding-right: 1.25rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.px-7 {
padding-left: 1.75rem;
padding-right: 1.75rem;
}
.px-8 {
padding-left: 2rem;
padding-right: 2rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.py-12 {
padding-top: 3rem;
padding-bottom: 3rem;
@ -845,10 +1041,45 @@ body {
padding-bottom: 0.75rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.py-6 {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pb-2 {
padding-bottom: 0.5rem;
}
.pt-0 {
padding-top: 0px;
}
.pt-2 {
padding-top: 0.5rem;
}
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.align-middle {
vertical-align: middle;
}
@ -908,16 +1139,35 @@ body {
letter-spacing: 0.025em;
}
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.text-card-foreground {
--tw-text-opacity: 1;
color: hsl(var(--card-foreground) / var(--tw-text-opacity));
}
.text-foreground {
--tw-text-opacity: 1;
color: hsl(var(--foreground) / var(--tw-text-opacity));
}
.text-foreground\/60 {
color: hsl(var(--foreground) / 0.6);
}
.text-muted-foreground {
--tw-text-opacity: 1;
color: hsl(var(--muted-foreground) / var(--tw-text-opacity));
}
.text-primary {
--tw-text-opacity: 1;
color: hsl(var(--primary) / var(--tw-text-opacity));
}
.text-primary-foreground {
--tw-text-opacity: 1;
color: hsl(var(--primary-foreground) / var(--tw-text-opacity));
@ -932,16 +1182,33 @@ body {
color: transparent;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0;
}
.opacity-100 {
opacity: 1;
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.outline {
outline-style: solid;
}
.ring-offset-background {
--tw-ring-offset-color: hsl(var(--background) / 1);
}
@ -956,12 +1223,51 @@ body {
transition-duration: 200ms;
}
.duration-300 {
transition-duration: 300ms;
}
.ease-in {
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
}
.ease-out {
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
/* fix for Alpine users. */
[x-cloak] {
display: none !important;
}
.file\:border-0::file-selector-button {
border-width: 0px;
}
.file\:bg-transparent::file-selector-button {
background-color: transparent;
}
.file\:text-sm::file-selector-button {
font-size: 0.875rem;
line-height: 1.25rem;
}
.file\:font-medium::file-selector-button {
font-weight: 500;
}
.placeholder\:text-muted-foreground::-moz-placeholder {
--tw-text-opacity: 1;
color: hsl(var(--muted-foreground) / var(--tw-text-opacity));
}
.placeholder\:text-muted-foreground::placeholder {
--tw-text-opacity: 1;
color: hsl(var(--muted-foreground) / var(--tw-text-opacity));
}
.placeholder\:text-neutral-500::-moz-placeholder {
--tw-text-opacity: 1;
color: rgb(115 115 115 / var(--tw-text-opacity));
@ -972,10 +1278,27 @@ body {
color: rgb(115 115 115 / var(--tw-text-opacity));
}
.hover\:bg-destructive\/80:hover {
background-color: hsl(var(--destructive) / 0.8);
}
.hover\:bg-primary\/90:hover {
background-color: hsl(var(--primary) / 0.9);
}
.hover\:bg-secondary:hover {
--tw-bg-opacity: 1;
background-color: hsl(var(--secondary) / var(--tw-bg-opacity));
}
.hover\:bg-secondary\/80:hover {
background-color: hsl(var(--secondary) / 0.8);
}
.hover\:text-foreground\/80:hover {
color: hsl(var(--foreground) / 0.8);
}
.focus\:border-neutral-300:focus {
--tw-border-opacity: 1;
border-color: rgb(212 212 212 / var(--tw-border-opacity));
@ -997,6 +1320,22 @@ body {
--tw-ring-color: hsl(var(--primary) / var(--tw-ring-opacity));
}
.focus-visible\:outline-none:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus-visible\:ring-1:focus-visible {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.focus-visible\:ring-ring:focus-visible {
--tw-ring-opacity: 1;
--tw-ring-color: hsl(var(--ring) / var(--tw-ring-opacity));
}
.disabled\:cursor-not-allowed:disabled {
cursor: not-allowed;
}
@ -1005,6 +1344,33 @@ body {
opacity: 0.5;
}
@media (min-width: 640px) {
.sm\:max-w-lg {
max-width: 32rem;
}
.sm\:translate-y-0 {
--tw-translate-y: 0px;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.sm\:scale-100 {
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.sm\:scale-95 {
--tw-scale-x: .95;
--tw-scale-y: .95;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.sm\:rounded-lg {
border-radius: var(--radius);
}
}
@media (min-width: 1024px) {
.lg\:mt-20 {
margin-top: 5rem;

View file

@ -7,7 +7,6 @@ import (
const (
DriverSqlite3 = "sqlite3"
DriverMysql = "mysql"
)
type Config struct {
@ -18,7 +17,7 @@ type Config struct {
Password string
}
func NewSQL(cfg Config) (*sql.DB, error) {
func New(cfg Config) (*sql.DB, error) {
switch cfg.Driver {
case DriverSqlite3:
name := cfg.Name

View file

@ -3,14 +3,17 @@ package event
import (
"context"
"reflect"
"sync"
"testing"
)
func TestEventSubscribeEmit(t *testing.T) {
expect := 1
ctx, cancel := context.WithCancel(context.Background())
Subscribe("foo.a", func(_ context.Context, event any) {
defer cancel()
var (
expect = 1
wg = sync.WaitGroup{}
)
wg.Add(1)
Subscribe("foo.bar", func(_ context.Context, event any) {
value, ok := event.(int)
if !ok {
t.Errorf("expected int got %v", reflect.TypeOf(event))
@ -18,15 +21,16 @@ func TestEventSubscribeEmit(t *testing.T) {
if value != 1 {
t.Errorf("expected %d got %d", expect, value)
}
wg.Done()
})
Emit("foo.a", expect)
<-ctx.Done()
Emit("foo.bar", expect)
wg.Wait()
}
func TestUnsubscribe(t *testing.T) {
sub := Subscribe("foo.b", func(_ context.Context, _ any) {})
sub := Subscribe("foo.bar", func(_ context.Context, _ any) {})
Unsubscribe(sub)
if _, ok := stream.subs["foo.b"]; ok {
if _, ok := stream.subs["foo.bar"]; ok {
t.Errorf("expected topic foo.bar to be deleted")
}
}

2
go.mod
View file

@ -3,7 +3,7 @@ module github.com/anthdm/superkit
go 1.22.0
require (
github.com/a-h/templ v0.2.731
github.com/a-h/templ v0.2.707
github.com/gorilla/sessions v1.3.0
github.com/stretchr/testify v1.9.0
)

2
go.sum
View file

@ -1,7 +1,5 @@
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/a-h/templ v0.2.731 h1:yiv4C7whSUsa36y65O06DPr/U/j3+WGB0RmvLOoVFXc=
github.com/a-h/templ v0.2.731/go.mod h1:IejA/ecDD0ul0dCvgCwp9t7bUZXVpGClEAdsqZQigi8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

View file

@ -4,14 +4,12 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http"
"os"
"github.com/a-h/templ"
"github.com/gorilla/sessions"
"github.com/joho/godotenv"
)
var store *sessions.CookieStore
@ -168,10 +166,7 @@ func Env() string {
// initialize the store here so the environment variables are
// already initialized. Calling NewCookieStore() from outside of
// a function scope won't work.
func Setup() {
if err := godotenv.Load(); err != nil {
log.Fatal(err)
}
func init() {
appSecret := os.Getenv("SUPERKIT_SECRET")
if len(appSecret) < 32 {
// For security reasons we are calling os.Exit(1) here so Go's panic recover won't

View file

@ -5,14 +5,11 @@ import (
"net/http"
)
type (
RequestKey struct{}
ResponseHeadersKey struct{}
)
type RequestURLKey struct{}
func WithRequest(next http.Handler) http.Handler {
func WithRequestURL(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), RequestKey{}, r)
ctx := context.WithValue(r.Context(), RequestURLKey{}, r.URL)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View file

@ -1,7 +1,7 @@
package button
import (
"github.com/anthdm/superkit/ui"
"AABBCCDD/ui"
"github.com/a-h/templ"
)

View file

@ -1,6 +1,6 @@
package card
import "github.com/anthdm/superkit/ui"
import "AABBCCDD/ui"
const cardBaseClass = "rounded-lg border bg-card text-card-foreground shadow-sm"

175
ui/card/card_templ.go Normal file
View file

@ -0,0 +1,175 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package card
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "AABBCCDD/ui"
const cardBaseClass = "rounded-lg border bg-card text-card-foreground shadow-sm"
func Card(opts ...func(*templ.Attributes)) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, ui.CreateAttrs(cardBaseClass, "", opts...))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Header(opts ...func(*templ.Attributes)) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, ui.CreateAttrs("p-6", "", opts...))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var2.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Content(opts ...func(*templ.Attributes)) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, ui.CreateAttrs("p-6 pt-0", "", opts...))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var3.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Footer(opts ...func(*templ.Attributes)) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, ui.CreateAttrs("p-6 pt-0", "", opts...))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var4.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View file

@ -3,7 +3,7 @@ package input
import (
"github.com/a-h/templ"
"github.com/anthdm/superkit/ui"
"AABBCCDD/ui"
)
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"

159
ui/modal/modal_templ.go Normal file
View file

@ -0,0 +1,159 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package modal
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
func Modal() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div x-data=\"{ modalOpen: false }\" @keydown.escape.window=\"modalOpen = false\" class=\"relative z-50 w-auto h-auto\"><div @click=\"modalOpen=true\">foo</div><template x-teleport=\"body\"><div x-show=\"modalOpen\" class=\"fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen\" x-cloak><div x-show=\"modalOpen\" x-transition:enter=\"ease-out duration-300\" x-transition:enter-start=\"opacity-0\" x-transition:enter-end=\"opacity-100\" x-transition:leave=\"ease-in duration-300\" x-transition:leave-start=\"opacity-100\" x-transition:leave-end=\"opacity-0\" @click=\"modalOpen=false\" class=\"absolute inset-0 w-full h-full bg-black bg-opacity-40\"></div><div x-show=\"modalOpen\" x-trap.inert.noscroll=\"modalOpen\" x-transition:enter=\"ease-out duration-300\" x-transition:enter-start=\"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\" x-transition:enter-end=\"opacity-100 translate-y-0 sm:scale-100\" x-transition:leave=\"ease-in duration-200\" x-transition:leave-start=\"opacity-100 translate-y-0 sm:scale-100\" x-transition:leave-end=\"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95\" class=\"relative w-full py-6 px-7 sm:max-w-lg sm:rounded-lg\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div @click=\"modalOpen=true\">foo</div></div></div></template></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Header() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex items-center justify-between pb-2\"><h3 class=\"text-lg font-semibold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var2.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h3><button @click=\"modalOpen=false\" class=\"absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 rounded-full\"><svg class=\"w-5 h-5\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18L18 6M6 6l12 12\"></path></svg></button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Content() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"relative w-auto\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var3.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Trigger() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div @click=\"modalOpen=true\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var4.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Footer() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View file

@ -1,7 +1,7 @@
package table
import (
"github.com/anthdm/superkit/ui"
"AABBCCDD/ui"
)
templ Table() {

172
ui/table/table_templ.go Normal file
View file

@ -0,0 +1,172 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.707
package table
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import (
"AABBCCDD/ui"
)
func Table() templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex flex-col border rounded-md\"><div class=\"overflow-x-auto\"><div class=\"inline-block min-w-full\"><div class=\"overflow-hidden\"><table class=\"min-w-full divide-y\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</table></div></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Header(opts ...func(*templ.Attributes)) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<thead")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, ui.CreateAttrs("", "", opts...))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("><tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var2.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</tr></thead>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Body(opts ...func(*templ.Attributes)) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tbody")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, ui.CreateAttrs("divide-y", "", opts...))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var3.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
func Footer(opts ...func(*templ.Attributes)) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tfoot><tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var4.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</tr></tfoot>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
const (
thBaseClass = "px-5 py-3 text-xs font-medium uppercase"
tdBaseClass = "px-5 py-4 text-sm whitespace-nowrap"
)
func Td(opts ...func(*templ.Attributes)) templ.Attributes {
return ui.CreateAttrs(tdBaseClass, "", opts...)
}
func Th(opts ...func(*templ.Attributes)) templ.Attributes {
return ui.CreateAttrs(thBaseClass, "", opts...)
}

View file

@ -1,4 +0,0 @@
# Validate
Schema based validation with superpowers for Golang.
# TODO

View file

@ -10,7 +10,7 @@ import (
var (
emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
urlRegex = regexp.MustCompile(`^(https?:\/\/)?(www\.)?([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,}(\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&'()*+,;=]*)?$`)
urlRegex = regexp.MustCompile(`^(http(s)?://)?([\da-z\.-]+)\.([a-z\.]{2,6})([/\w \.-]*)*/?$`)
)
// RuleSet holds the state of a single rule.

View file

@ -39,7 +39,7 @@ func (e Errors) Has(field string) bool {
// Schema represents a validation schema.
type Schema map[string][]RuleSet
// Merge merges the two given schemas, returning a new Schema.
// Merge merges the two given schemas returning a new Schema.
func Merge(schema, other Schema) Schema {
newSchema := Schema{}
maps.Copy(newSchema, schema)
@ -75,14 +75,9 @@ func Request(r *http.Request, data any, schema Schema) (Errors, bool) {
func validate(data any, schema Schema, errors Errors) (Errors, bool) {
ok := true
for fieldName, ruleSets := range schema {
// Uppercase the field name so we never check un-exported fields.
// But we need to watch out for member fields that are uppercased by
// the user. For example (URL, ID, ...)
if !isUppercase(fieldName) {
fieldName = string(unicode.ToUpper(rune(fieldName[0]))) + fieldName[1:]
}
fieldValue := getFieldAndTagByName(data, fieldName)
// Uppercase the field name so we never check un-exported fields
fieldName = string(unicode.ToUpper(rune(fieldName[0]))) + fieldName[1:]
fieldValue := getFieldValueByName(data, fieldName)
for _, set := range ruleSets {
set.FieldValue = fieldValue
set.FieldName = fieldName
@ -103,7 +98,7 @@ func validate(data any, schema Schema, errors Errors) (Errors, bool) {
return errors, ok
}
func getFieldAndTagByName(v any, name string) any {
func getFieldValueByName(v any, name string) any {
val := reflect.ValueOf(v)
if val.Kind() == reflect.Ptr {
val = val.Elem()
@ -153,18 +148,12 @@ func parseRequest(r *http.Request, v any) error {
}
case reflect.String:
fieldVal.SetString(formValue)
case reflect.Int, reflect.Int32, reflect.Int64:
case reflect.Int:
intVal, err := strconv.Atoi(formValue)
if err != nil {
return fmt.Errorf("failed to parse int: %v", err)
}
fieldVal.SetInt(int64(intVal))
case reflect.Uint, reflect.Uint32, reflect.Uint64:
intVal, err := strconv.Atoi(formValue)
if err != nil {
return fmt.Errorf("failed to parse int: %v", err)
}
fieldVal.SetUint(uint64(intVal))
case reflect.Float64:
floatVal, err := strconv.ParseFloat(formValue, 64)
if err != nil {
@ -179,12 +168,3 @@ func parseRequest(r *http.Request, v any) error {
}
return nil
}
func isUppercase(s string) bool {
for _, ch := range s {
if !unicode.IsUpper(rune(ch)) {
return false
}
}
return true
}

View file

@ -2,9 +2,6 @@ package validate
import (
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"time"
@ -31,65 +28,6 @@ var testSchema = Schema{
"username": Rules(Required),
}
func TestValidateRequest(t *testing.T) {
var (
email = "foo@bar.com"
password = "superHunter123@"
firstName = "Anthony"
website = "http://foo.com"
randomNumber = 123
randomFloat = 9.999
)
formValues := url.Values{}
formValues.Set("email", email)
formValues.Set("password", password)
formValues.Set("firstName", firstName)
formValues.Set("url", website)
formValues.Set("brandom", fmt.Sprint(randomNumber))
formValues.Set("arandom", fmt.Sprint(randomFloat))
encodedValues := formValues.Encode()
req, err := http.NewRequest("POST", "http://foo.com", strings.NewReader(encodedValues))
assert.Nil(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
type SignupData struct {
Email string `form:"email"`
Password string `form:"password"`
FirstName string `form:"firstName"`
URL string `form:"url"`
ARandomRenamedNumber int `form:"brandom"`
ARandomRenamedFloat float64 `form:"arandom"`
}
schema := Schema{
"Email": Rules(Email),
"Password": Rules(
Required,
ContainsDigit,
ContainsUpper,
ContainsSpecial,
Min(7),
),
"FirstName": Rules(Min(3), Max(50)),
"URL": Rules(URL),
"ARandomRenamedNumber": Rules(GT(100), LT(124)),
"ARandomRenamedFloat": Rules(GT(9.0), LT(10.1)),
}
var data SignupData
errors, ok := Request(req, &data, schema)
assert.True(t, ok)
assert.Empty(t, errors)
assert.Equal(t, data.Email, email)
assert.Equal(t, data.Password, password)
assert.Equal(t, data.FirstName, firstName)
assert.Equal(t, data.URL, website)
assert.Equal(t, data.ARandomRenamedNumber, randomNumber)
assert.Equal(t, data.ARandomRenamedFloat, randomFloat)
}
func TestTime(t *testing.T) {
type Foo struct {
CreatedAt time.Time
@ -110,89 +48,21 @@ func TestTime(t *testing.T) {
func TestURL(t *testing.T) {
type Foo struct {
URL string `v:"URL"`
URL string
}
foo := Foo{
URL: "not an url",
}
schema := Schema{
"URL": Rules(URL),
"url": Rules(URL),
}
errors, ok := Validate(foo, schema)
assert.False(t, ok)
assert.NotEmpty(t, errors)
validURLS := []string{
"http://google.com",
"http://www.google.com",
"https://www.google.com",
"https://www.google.com",
"www.google.com",
"https://book.com/sales",
"app.book.com",
"app.book.com/signup",
}
for _, url := range validURLS {
foo.URL = url
errors, ok = Validate(foo, schema)
assert.True(t, ok)
assert.Empty(t, errors)
}
}
func TestContainsUpper(t *testing.T) {
type Foo struct {
Password string
}
foo := Foo{"hunter"}
schema := Schema{
"Password": Rules(ContainsUpper),
}
errors, ok := Validate(foo, schema)
assert.False(t, ok)
assert.NotEmpty(t, errors)
foo.Password = "Hunter"
foo.URL = "www.user.com"
errors, ok = Validate(foo, schema)
assert.True(t, ok)
assert.Empty(t, errors)
}
func TestContainsDigit(t *testing.T) {
type Foo struct {
Password string
}
foo := Foo{"hunter"}
schema := Schema{
"Password": Rules(ContainsDigit),
}
errors, ok := Validate(foo, schema)
assert.False(t, ok)
assert.NotEmpty(t, errors)
foo.Password = "Hunter1"
errors, ok = Validate(foo, schema)
assert.True(t, ok)
assert.Empty(t, errors)
}
func TestContainsSpecial(t *testing.T) {
type Foo struct {
Password string
}
foo := Foo{"hunter"}
schema := Schema{
"Password": Rules(ContainsSpecial),
}
errors, ok := Validate(foo, schema)
assert.False(t, ok)
assert.NotEmpty(t, errors)
foo.Password = "Hunter@"
errors, ok = Validate(foo, schema)
assert.True(t, ok)
assert.Empty(t, errors)
fmt.Println(errors)
}
func TestRuleIn(t *testing.T) {

View file

@ -3,7 +3,6 @@ package view
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/anthdm/superkit/kit"
@ -41,13 +40,5 @@ func Auth(ctx context.Context) kit.Auth {
//
// view.URL(ctx).Path // => ex. /login
func URL(ctx context.Context) *url.URL {
return getContextValue(ctx, middleware.RequestKey{}, &http.Request{}).URL
}
// Request is a view helper that returns the current http request.
// The request can be accessed with:
//
// view.Request(ctx)
func Request(ctx context.Context) *http.Request {
return getContextValue(ctx, middleware.RequestKey{}, &http.Request{})
return getContextValue(ctx, middleware.RequestURLKey{}, &url.URL{})
}