bootstrapped

This commit is contained in:
anthdm 2024-06-06 10:04:51 +02:00
parent 5721ea14a9
commit db69f3b519
36 changed files with 369 additions and 192 deletions

View file

@ -41,11 +41,14 @@ live/sync_assets:
dev:
@make -j5 live/templ live/server live/tailwind live/sync_assets
reset:
@GOOSE_DRIVER=postgres GOOSE_DBSTRING=$(dsn) goose -dir=$(migrationPath) reset
db-status:
GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) goose status
up:
@GOOSE_DRIVER=postgres GOOSE_DBSTRING=$(dsn) goose -dir=$(migrationPath) up
db-reset:
@GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) goose -dir=$(MIGRATION_DIR) reset
seed:
@go run scripts/seed/main.go
db-up:
@GOOSE_DRIVER=$(DB_DRIVER) GOOSE_DBSTRING=$(DB_NAME) goose -dir=$(MIGRATION_DIR) up
db-seed:
@go run cmd/scripts/seed/main.go

View file

@ -0,0 +1,3 @@
package conf
// Application config

34
bootstrap/app/db/db.go Normal file
View file

@ -0,0 +1,34 @@
package db
import (
"log"
"os"
"github.com/anthdm/gothkit/pkg/db"
"github.com/anthdm/gothkit/pkg/kit"
_ "github.com/mattn/go-sqlite3"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/extra/bundebug"
)
var Query *bun.DB
func init() {
config := db.Config{
Driver: os.Getenv("DB_DRIVER"),
Name: os.Getenv("DB_NAME"),
Password: os.Getenv("DB_PASSWORD"),
User: os.Getenv("DB_USER"),
Host: os.Getenv("DB_HOST"),
}
db, err := db.New(config)
if err != nil {
log.Fatal(err)
}
Query = bun.NewDB(db, sqlitedialect.New())
if kit.IsDevelopment() {
Query.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
}
}

View file

@ -0,0 +1,13 @@
package handlers
import (
"net/http"
"example-app/app/types"
"github.com/anthdm/gothkit/pkg/kit"
)
func HandleAuthentication(w http.ResponseWriter, r *http.Request) (kit.Auth, error) {
return types.AuthUser{}, nil
}

View file

@ -0,0 +1,13 @@
package handlers
import (
"example-app/app/db"
"example-app/app/views/landing"
"github.com/anthdm/gothkit/pkg/kit"
)
func HandleLandingIndex(kit *kit.Kit) error {
db.Query.NewSelect().Scan(kit.Request.Context())
return kit.Render(landing.Index())
}

61
bootstrap/app/routes.go Normal file
View file

@ -0,0 +1,61 @@
package app
import (
"example-app/app/handlers"
"example-app/app/views/errors"
"log/slog"
"github.com/anthdm/gothkit/pkg/kit"
"github.com/anthdm/gothkit/pkg/kit/middleware"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
)
// Define your global middleware
func InitializeMiddleware(router *chi.Mux) {
router.Use(chimiddleware.Logger)
router.Use(chimiddleware.Recoverer)
router.Use(middleware.WithRequestURL)
}
// Define your routes in here
func InitializeRoutes(router *chi.Mux) {
// Comment out to configure your authentication
authConfig := kit.AuthenticationConfig{
AuthFunc: handlers.HandleAuthentication,
RedirectURL: "/login",
}
// Routes that "might" have an authenticated user
router.Group(func(app chi.Router) {
app.Use(kit.WithAuthentication(authConfig, false)) // strict set to false
// Routes
app.Get("/", kit.Handler(handlers.HandleLandingIndex))
})
// Authenticated routes
//
// Routes that "must" have an authenticated user or else they
// will be redirected to the configured redirectURL, set in the
// AuthenticationConfig.
router.Group(func(app chi.Router) {
app.Use(kit.WithAuthentication(authConfig, true)) // strict set to true
// Routes
// app.Get("/path", kit.Handler(myHandler.HandleIndex))
})
}
// NotFoundHandler that will be called when the requested path could
// not be found.
func NotFoundHandler(kit *kit.Kit) error {
return kit.Render(errors.Error404())
}
// ErrorHandler that will be called on errors return from application handlers.
func ErrorHandler(kit *kit.Kit, err error) {
slog.Error("internal server error", "err", err.Error(), "path", kit.Request.URL.Path)
kit.Render(errors.Error500())
}

View file

@ -0,0 +1,14 @@
package types
// AuthUser represents an user that might be authenticated.
type AuthUser struct {
ID int
Email string
LoggedIn bool
}
// Check should return true if the user is authenticated.
// See handlers/auth.go.
func (user AuthUser) Check() bool {
return user.ID > 0 && user.LoggedIn
}

View file

@ -1,10 +1,10 @@
package errors
import (
"example-app/views/layouts"
"example-app/app/views/layouts"
)
templ Error400() {
templ Error404() {
@layouts.BaseLayout() {
<div class="h-screen w-full flex flex-col justify-center align-middle items-center gap-4">
<div class="text-muted-foreground text-5xl font-bold">404</div>

View file

@ -1,6 +1,6 @@
package errors
import "example-app/views/layouts"
import "example-app/app/views/layouts"
templ Error500() {
@layouts.BaseLayout() {

View file

@ -0,0 +1,18 @@
package landing
import (
"github.com/anthdm/gothkit/pkg/view"
"example-app/app/views/layouts"
)
templ Index() {
@layouts.App() {
<div class="mt-20">
<h1 class="text-3xl lg:text-6xl font-bold text-center">Ship stuff with small teams</h1>
</div>
<div>{ view.URL(ctx).Path }</div>
if view.Auth(ctx).Check() {
<div>You are authenticated</div>
}
}
}

View file

@ -1,6 +1,6 @@
package layouts
import "example-app/views/components"
import "example-app/app/views/components"
templ App() {
@BaseLayout() {

View file

@ -1,33 +1,30 @@
package main
import (
"log"
"log/slog"
"example-app/app"
"fmt"
"net/http"
"os"
"example-app/db"
"github.com/anthdm/gothkit/pkg/kit"
"github.com/go-chi/chi/v5"
)
func main() {
db, err := db.New()
if err != nil {
log.Fatal(err)
}
_ = db
// Routes configuration
router := chi.NewMux()
if true {
router.Handle("/*", disableCache(staticDev()))
}
initializeRoutes(router, db)
listenAddr := os.Getenv("HTTP_LISTEN_ADDR")
slog.Info("application started", "listenAddr", listenAddr)
app.InitializeMiddleware(router)
if kit.IsDevelopment() {
router.Handle("/public/*", disableCache(staticDev()))
}
kit.UseErrorHandler(app.ErrorHandler)
router.HandleFunc("/*", kit.Handler(app.NotFoundHandler))
app.InitializeRoutes(router)
fmt.Printf("application running in %s at %s\n", kit.Env(), "http://localhost:7331")
http.ListenAndServe(os.Getenv("HTTP_LISTEN_ADDR"), router)
}

View file

@ -0,0 +1,7 @@
package main
import "fmt"
func main() {
fmt.Println("there are no seeds.")
}

29
bootstrap/go.mod Normal file
View file

@ -0,0 +1,29 @@
module example-app
go 1.22.0
require (
github.com/anthdm/gothkit v0.0.0-00010101000000-000000000000
github.com/go-chi/chi/v5 v5.0.12
)
require (
github.com/fatih/color v1.16.0 // indirect
github.com/jinzhu/inflection v1.0.0 // 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.19.0 // indirect
)
require (
github.com/a-h/templ v0.2.707 // indirect
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
)
replace github.com/anthdm/gothkit => ../gothkit

33
bootstrap/go.sum Normal file
View file

@ -0,0 +1,33 @@
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/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/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/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/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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View file

31
gothkit/pkg/db/db.go Normal file
View file

@ -0,0 +1,31 @@
package db
import (
"database/sql"
"fmt"
)
const (
DriverSqlite3 = "sqlite3"
)
type Config struct {
Driver string
Name string
Host string
User string
Password string
}
func New(cfg Config) (*sql.DB, error) {
switch cfg.Driver {
case DriverSqlite3:
name := cfg.Name
if len(name) == 0 {
name = "app_db"
}
return sql.Open(cfg.Driver, name)
default:
return nil, fmt.Errorf("invalid database driver (%s): currently only sqlite3 is supported", cfg.Driver)
}
}

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"log/slog"
"net/http"
"os"
"github.com/a-h/templ"
)
@ -24,15 +25,14 @@ type Auth interface {
}
var (
auth Auth = defaultAuth{}
errorHandler = func(kit *Kit, err error) {
errorHandler = func(kit *Kit, err error) {
kit.Text(http.StatusInternalServerError, err.Error())
}
)
type defaultAuth struct{}
type DefaultAuth struct{}
func (defaultAuth) Check() bool { return false }
func (DefaultAuth) Check() bool { return false }
type Kit struct {
Response http.ResponseWriter
@ -40,19 +40,28 @@ type Kit struct {
}
func UseErrorHandler(h ErrorHandlerFunc) { errorHandler = h }
func SetAuth(a Auth) { auth = a }
func (kit *Kit) Auth() Auth {
value, ok := kit.Request.Context().Value(AuthKey{}).(Auth)
if !ok {
slog.Warn("kit authentication not set")
return auth
return DefaultAuth{}
}
return value
}
func (kit *Kit) Redirect(status int, url string) {
func (kit *Kit) Redirect(status int, url string) error {
http.Redirect(kit.Response, kit.Request, url, status)
return nil
}
func (kit *Kit) HXRedirect(status int, url string) error {
if len(kit.Request.Header.Get("HX-Request")) > 0 {
kit.Response.Header().Set("HX-Redirect", url)
kit.Response.WriteHeader(http.StatusSeeOther)
return nil
}
return kit.Redirect(status, url)
}
func (kit *Kit) JSON(status int, v any) error {
@ -123,3 +132,15 @@ func WithAuthentication(config AuthenticationConfig, strict bool) func(http.Hand
})
}
}
func IsDevelopment() bool {
return os.Getenv("KIT_ENV") == "development"
}
func IsProduction() bool {
return os.Getenv("KIT_ENV") == "production"
}
func Env() string {
return os.Getenv("KIT_ENV")
}

View file

@ -1,14 +1,15 @@
package middleware
// func Authenticated(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// kit := &kit.Kit{
// Response: w,
// Request: r,
// }
// if !kit.Auth().LoggedIn {
import (
"context"
"net/http"
)
// }
// next.ServeHTTP(w, r.WithContext(ctx))
// })
// }
type RequestURLKey struct{}
func WithRequestURL(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), RequestURLKey{}, r.URL)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

31
gothkit/pkg/view/view.go Normal file
View file

@ -0,0 +1,31 @@
package view
import (
"context"
"net/url"
"github.com/anthdm/gothkit/pkg/kit"
"github.com/anthdm/gothkit/pkg/kit/middleware"
)
// Auth is a view helper function that returns the currency Auth.
// If Auth is not set a default auth will be returned
func Auth(ctx context.Context) kit.Auth {
value, ok := ctx.Value(kit.AuthKey{}).(kit.Auth)
if !ok {
return kit.DefaultAuth{}
}
return value
}
// URL is a view helper that returns the current URL.
// The request path can be acccessed with:
// view.URL(ctx).Path
func URL(ctx context.Context) *url.URL {
value, ok := ctx.Value(middleware.RequestURLKey{}).(*url.URL)
if !ok {
return &url.URL{}
}
return value
}

View file

@ -1 +1,14 @@
echo "this is working fine"
#!/bin/bash
echo "enter the name of you project: "
read varname
git clone git@github.com:anthdm/gothkit.git _gothkit
cd _gothkit
mv bootstrap ../$varname
cd ..
rm -rf _gothkit
echo ""
echo "project installed successfully"
echo "your project folder is available => cd $varname"

View file

@ -1,61 +0,0 @@
package main
import (
"database/sql"
"log/slog"
"net/http"
"github.com/anthdm/gothkit/pkg/kit"
"github.com/go-chi/chi/v5"
"example-app/handlers"
"example-app/views/errors"
)
// Define your routes in here
func initializeRoutes(router *chi.Mux, db *sql.DB) {
// Configure the error handler
kit.UseErrorHandler(func(kit *kit.Kit, err error) {
slog.Error("internal server error", "err", err.Error(), "path", kit.Request.URL.Path)
kit.Render(errors.Error500())
})
// Comment out to configure your authentication
authConfig := kit.AuthenticationConfig{
AuthFunc: handleAuthentication,
RedirectURL: "/login",
}
landingHandler := handlers.NewLandingHandler(db)
// Routes that "might" have an authenticated user
router.Group(func(app chi.Router) {
app.Use(kit.WithAuthentication(authConfig, false)) // strict set to false
// Routes
app.Get("/", kit.Handler(landingHandler.HandleIndex))
})
// Routes that "must" have an authenticated user or else they
// will be redirected to the configured redirectURL, set in the
// AuthenticationConfig.
router.Group(func(app chi.Router) {
app.Use(kit.WithAuthentication(authConfig, true)) // strict set to true
// Routes
// app.Get("/path", kit.Handler(myHandler.HandleIndex))
})
}
type AuthUser struct {
ID int
Email string
LoggedIn bool
}
func (user AuthUser) Check() bool {
return user.ID > 0 && user.LoggedIn
}
func handleAuthentication(w http.ResponseWriter, r *http.Request) (kit.Auth, error) {
return AuthUser{}, nil
}

View file

@ -1,28 +0,0 @@
package db
import (
"database/sql"
"fmt"
"os"
_ "github.com/mattn/go-sqlite3"
)
const (
DriverSqlite3 = "sqlite3"
)
func New() (*sql.DB, error) {
driver := os.Getenv("DB_DRIVER")
switch driver {
case DriverSqlite3:
name := os.Getenv("DB_NAME")
if len(name) == 0 {
name = "gothkit"
}
return sql.Open(driver, name)
default:
return nil, fmt.Errorf("invalid database driver (%s): currently only sqlite3 is supported", driver)
}
}

View file

@ -1,15 +0,0 @@
module example-app
go 1.22.0
require (
github.com/anthdm/gothkit v0.0.0-00010101000000-000000000000
github.com/go-chi/chi/v5 v5.0.12
)
require (
github.com/a-h/templ v0.2.707 // indirect
github.com/mattn/go-sqlite3 v1.14.22
)
replace github.com/anthdm/gothkit => ../gothkit

View file

@ -1,8 +0,0 @@
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/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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=

View file

@ -1,22 +0,0 @@
package handlers
import (
"database/sql"
"example-app/views/landing"
"github.com/anthdm/gothkit/pkg/kit"
)
type LandingHandler struct {
db *sql.DB
}
func NewLandingHandler(db *sql.DB) *LandingHandler {
return &LandingHandler{
db: db,
}
}
func (h *LandingHandler) HandleIndex(kit *kit.Kit) error {
return kit.Render(landing.Index())
}

View file

@ -1,11 +0,0 @@
package landing
import "example-app/views/layouts"
templ Index() {
@layouts.App() {
<div class="mt-20">
<h1 class="text-3xl lg:text-6xl font-bold text-center">Ship stuff with small teams</h1>
</div>
}
}