From e100f2d56ba1bc1886301a6df9c881ff57ef42c1 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 15 Aug 2025 14:50:14 -0500 Subject: [PATCH] make params an any slice to support automatic int conversion, add readme --- README.md | 421 ++++++++++++++++++++++++++++++++++++++++++ auth/auth.go | 6 +- csrf/csrf.go | 2 +- fs.go | 6 +- router.go | 12 +- session/middleware.go | 2 +- sushi.go | 4 +- timing/timing.go | 2 +- types.go | 4 +- 9 files changed, 440 insertions(+), 19 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..023dc1d --- /dev/null +++ b/README.md @@ -0,0 +1,421 @@ +# 🍣 Sushi + +A fast, raw, tasty framework for simplifying basic web apps! + +## Quick Start + +```go +package main + +import ( + "git.sharkk.net/Sharkk/Sushi" + "git.sharkk.net/Sharkk/Sushi/session" +) + +func main() { + app := sushi.New() + + // Initialize sessions + session.InitSessions("sessions.json") + + app.Get("/", func(ctx sushi.Ctx, params []any) { + sushi.SendHTML(ctx, "

Hello Sushi!

") + }) + + app.Listen(":8080") +} +``` + +## Routing + +```go +// Basic routes +app.Get("/users/:id", getUserHandler) +app.Post("/users", createUserHandler) +app.Put("/users/:id", updateUserHandler) +app.Delete("/users/:id", deleteUserHandler) + +// Wildcards +app.Get("/files/*path", serveFilesHandler) + +// Route groups +api := app.Group("/api/v1") +api.Get("/users", listUsersHandler) +api.Post("/users", createUserHandler) +``` + +## Parameters + +URL parameters are automatically converted to the appropriate type: + +```go +// Numeric parameters become integers +app.Get("/users/:id", func(ctx sushi.Ctx, params []any) { + userID := params[0].(int) // /users/123 -> 123 + // ... +}) + +// String parameters stay strings +app.Get("/users/:name", func(ctx sushi.Ctx, params []any) { + name := params[0].(string) // /users/john -> "john" + // ... +}) + +// Mixed types +app.Get("/users/:id/posts/:slug", func(ctx sushi.Ctx, params []any) { + userID := params[0].(int) // 123 + slug := params[1].(string) // "my-post" + // ... +}) +``` + +## Response Helpers + +```go +func myHandler(ctx sushi.Ctx, params []any) { + // JSON responses + sushi.SendJSON(ctx, map[string]string{"message": "success"}) + + // HTML responses + sushi.SendHTML(ctx, "

Welcome

") + + // Text responses + sushi.SendText(ctx, "Plain text") + + // Error responses + sushi.SendError(ctx, 404, "Not Found") + + // Redirects + sushi.SendRedirect(ctx, "/login") + + // Status only + sushi.SendStatus(ctx, 204) +} +``` + +## Middleware + +```go +// Custom middleware +func loggingMiddleware() sushi.Middleware { + return func(ctx sushi.Ctx, params []any, next func()) { + println("Request:", string(ctx.Method()), string(ctx.Path())) + next() + println("Status:", ctx.Response.StatusCode()) + } +} + +app.Use(loggingMiddleware()) + +// Group middleware +admin := app.Group("/admin") +admin.Use(auth.RequireAuth("/login")) +``` + +## Authentication Workflow + +### 1. Setup Password Hashing + +```go +import "git.sharkk.net/Sharkk/Sushi/password" + +// Hash password for storage +hashedPassword := password.HashPassword("userpassword123") + +// Verify password during login +isValid, err := password.VerifyPassword("userpassword123", hashedPassword) +``` + +### 2. User Structure + +```go +type User struct { + ID int `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + Password string `json:"password"` // Store hashed password +} + +// User lookup function for auth middleware +func getUserByID(userID int) any { + // Query your database for user by ID + // Return nil if not found + return &User{ID: userID, Email: "user@example.com"} +} +``` + +### 3. Session & Auth Middleware + +```go +import ( + "git.sharkk.net/Sharkk/Sushi/session" + "git.sharkk.net/Sharkk/Sushi/auth" +) + +func main() { + app := sushi.New() + + // Initialize sessions + session.InitSessions("sessions.json") + + // Add session middleware + app.Use(session.Middleware()) + + // Add auth middleware with user lookup + app.Use(auth.Middleware(getUserByID)) + + // Public routes + app.Get("/login", loginPageHandler) + app.Post("/login", loginHandler) + app.Post("/logout", logoutHandler) + + // Protected routes + protected := app.Group("/dashboard") + protected.Use(auth.RequireAuth("/login")) + protected.Get("/", dashboardHandler) +} +``` + +### 4. Login Handler + +```go +func loginHandler(ctx sushi.Ctx, params []string) { + email := string(ctx.PostArgs().Peek("email")) + password := string(ctx.PostArgs().Peek("password")) + + // Find user by email/username + user := findUserByEmail(email) + if user == nil { + sushi.SendError(ctx, 401, "Invalid credentials") + return + } + + // Verify password + isValid, err := password.VerifyPassword(password, user.Password) + if err != nil || !isValid { + sushi.SendError(ctx, 401, "Invalid credentials") + return + } + + // Log the user in + auth.Login(ctx, user.ID, user) + + sushi.SendRedirect(ctx, "/dashboard") +} +``` + +### 5. Logout Handler + +```go +func logoutHandler(ctx sushi.Ctx, params []string) { + auth.Logout(ctx) + sushi.SendRedirect(ctx, "/") +} +``` + +### 6. Getting Current User + +```go +func dashboardHandler(ctx sushi.Ctx, params []string) { + user := auth.GetCurrentUser(ctx).(*User) + + html := fmt.Sprintf("

Welcome, %s!

", user.Username) + sushi.SendHTML(ctx, html) +} +``` + +## CSRF Protection + +```go +import "git.sharkk.net/Sharkk/Sushi/csrf" + +// Add CSRF middleware to forms +app.Use(csrf.Middleware()) + +// In your form template +func loginPageHandler(ctx sushi.Ctx, params []string) { + csrfField := csrf.CSRFHiddenField(ctx) + + html := fmt.Sprintf(` +
+ %s + + + +
+ `, csrfField) + + sushi.SendHTML(ctx, html) +} +``` + +## Static Files + +```go +// Serve static files +app.Get("/static/*path", sushi.Static("./public")) + +// Serve single file +app.Get("/favicon.ico", sushi.StaticFile("./public/favicon.ico")) + +// Embedded files +files := map[string][]byte{ + "/style.css": cssData, + "/app.js": jsData, +} +app.Get("/assets/*path", sushi.StaticEmbed(files)) +``` + +## Sessions + +```go +func someHandler(ctx sushi.Ctx, params []string) { + sess := session.GetCurrentSession(ctx) + + // Set session data + sess.Set("user_preference", "dark_mode") + + // Get session data + if pref, exists := sess.Get("user_preference"); exists { + preference := pref.(string) + } + + // Flash messages (one-time) + sess.SetFlash("success", "Profile updated!") + + // Get flash message + message := sess.GetFlashMessage("success") +} +``` + +## Server Configuration + +```go +app := sushi.New(sushi.ServerOptions{ + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + MaxRequestBodySize: 10 * 1024 * 1024, // 10MB +}) + +// TLS +app.ListenTLS(":443", "cert.pem", "key.pem") +``` + +## Complete Auth Example + +```go +package main + +import ( + "fmt" + sushi "git.sharkk.net/Sharkk/Sushi" + "git.sharkk.net/Sharkk/Sushi/auth" + "git.sharkk.net/Sharkk/Sushi/csrf" + "git.sharkk.net/Sharkk/Sushi/password" + "git.sharkk.net/Sharkk/Sushi/session" +) + +type User struct { + ID int + Email string + Password string +} + +var users = map[int]*User{ + 1: {ID: 1, Email: "admin@example.com", Password: password.HashPassword("admin123")}, +} + +func getUserByID(userID int) any { + return users[userID] +} + +func findUserByEmail(email string) *User { + for _, user := range users { + if user.Email == email { + return user + } + } + return nil +} + +func main() { + app := sushi.New() + + session.InitSessions("sessions.json") + app.Use(session.Middleware()) + app.Use(auth.Middleware(getUserByID)) + + // Public routes + app.Get("/", homeHandler) + app.Get("/login", loginPageHandler) + app.Post("/login", loginHandler) + + // Protected routes + protected := app.Group("/dashboard") + protected.Use(auth.RequireAuth("/login")) + protected.Use(csrf.Middleware()) + protected.Get("/", dashboardHandler) + protected.Post("/logout", logoutHandler) + + app.Listen(":8080") +} + +func homeHandler(ctx sushi.Ctx, params []string) { + if auth.IsAuthenticated(ctx) { + sushi.SendRedirect(ctx, "/dashboard") + return + } + sushi.SendHTML(ctx, `Login`) +} + +func loginPageHandler(ctx sushi.Ctx, params []string) { + html := fmt.Sprintf(` +
+ %s +
+
+ +
+ `, csrf.CSRFHiddenField(ctx)) + + sushi.SendHTML(ctx, html) +} + +func loginHandler(ctx sushi.Ctx, params []string) { + email := string(ctx.PostArgs().Peek("email")) + pass := string(ctx.PostArgs().Peek("password")) + + user := findUserByEmail(email) + if user == nil { + sushi.SendError(ctx, 401, "Invalid credentials") + return + } + + if valid, _ := password.VerifyPassword(pass, user.Password); !valid { + sushi.SendError(ctx, 401, "Invalid credentials") + return + } + + auth.Login(ctx, user.ID, user) + sushi.SendRedirect(ctx, "/dashboard") +} + +func dashboardHandler(ctx sushi.Ctx, params []string) { + user := auth.GetCurrentUser(ctx).(*User) + + html := fmt.Sprintf(` +

Welcome, %s!

+
+ %s + +
+ `, user.Email, csrf.CSRFHiddenField(ctx)) + + sushi.SendHTML(ctx, html) +} + +func logoutHandler(ctx sushi.Ctx, params []string) { + auth.Logout(ctx) + sushi.SendRedirect(ctx, "/") +} +``` diff --git a/auth/auth.go b/auth/auth.go index 5084724..63fe9cd 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -10,7 +10,7 @@ const UserCtxKey = "user" // Middleware adds authentication handling func Middleware(userLookup func(int) any) sushi.Middleware { - return func(ctx sushi.Ctx, params []string, next func()) { + return func(ctx sushi.Ctx, params []any, next func()) { sess := session.GetCurrentSession(ctx) if sess != nil && sess.UserID > 0 && userLookup != nil { user := userLookup(sess.UserID) @@ -32,7 +32,7 @@ func RequireAuth(redirectPath ...string) sushi.Middleware { redirect = redirectPath[0] } - return func(ctx sushi.Ctx, params []string, next func()) { + return func(ctx sushi.Ctx, params []any, next func()) { if !IsAuthenticated(ctx) { ctx.Redirect(redirect, fasthttp.StatusFound) return @@ -48,7 +48,7 @@ func RequireGuest(redirectPath ...string) sushi.Middleware { redirect = redirectPath[0] } - return func(ctx sushi.Ctx, params []string, next func()) { + return func(ctx sushi.Ctx, params []any, next func()) { if IsAuthenticated(ctx) { ctx.Redirect(redirect, fasthttp.StatusFound) return diff --git a/csrf/csrf.go b/csrf/csrf.go index d56c6e5..44293f9 100644 --- a/csrf/csrf.go +++ b/csrf/csrf.go @@ -119,7 +119,7 @@ func ValidateFormCSRFToken(ctx sushi.Ctx) bool { // Middleware returns middleware that automatically validates CSRF tokens func Middleware() sushi.Middleware { - return func(ctx sushi.Ctx, params []string, next func()) { + return func(ctx sushi.Ctx, params []any, next func()) { method := string(ctx.Method()) if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" { diff --git a/fs.go b/fs.go index 52eaf4e..776b98d 100644 --- a/fs.go +++ b/fs.go @@ -50,7 +50,7 @@ func StaticFS(fsOptions StaticOptions) Handler { fsHandler := fs.NewRequestHandler() - return func(ctx Ctx, params []string) { + return func(ctx Ctx, params []any) { fsHandler(ctx) } } @@ -62,14 +62,14 @@ func Static(root string) Handler { // StaticFile serves a single file func StaticFile(filePath string) Handler { - return func(ctx Ctx, params []string) { + return func(ctx Ctx, params []any) { fasthttp.ServeFile(ctx, filePath) } } // StaticEmbed creates a handler for embedded files func StaticEmbed(files map[string][]byte) Handler { - return func(ctx Ctx, params []string) { + return func(ctx Ctx, params []any) { requestPath := string(ctx.Path()) // Try to find the file diff --git a/router.go b/router.go index d188abb..ea68302 100644 --- a/router.go +++ b/router.go @@ -22,7 +22,7 @@ type Router struct { patch *node delete *node middleware []Middleware - paramsBuffer []string + paramsBuffer []any } type Group struct { @@ -40,7 +40,7 @@ func NewRouter() *Router { patch: &node{}, delete: &node{}, middleware: []Middleware{}, - paramsBuffer: make([]string, 64), + paramsBuffer: make([]any, 64), } } @@ -145,7 +145,7 @@ func applyMiddleware(h Handler, mw []Middleware) Handler { return h } - return func(ctx Ctx, params []string) { + return func(ctx Ctx, params []any) { var index int var next func() @@ -229,7 +229,7 @@ func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) e } // Lookup finds a handler matching method and path -func (r *Router) Lookup(method, path string) (Handler, []string, bool) { +func (r *Router) Lookup(method, path string) (Handler, []any, bool) { root := r.methodNode(method) if root == nil { return nil, nil, false @@ -240,7 +240,7 @@ func (r *Router) Lookup(method, path string) (Handler, []string, bool) { buffer := r.paramsBuffer if cap(buffer) < int(root.maxParams) { - buffer = make([]string, root.maxParams) + buffer = make([]any, root.maxParams) r.paramsBuffer = buffer } buffer = buffer[:0] @@ -253,7 +253,7 @@ func (r *Router) Lookup(method, path string) (Handler, []string, bool) { return h, buffer[:paramCount], true } -func match(current *node, path string, start int, params *[]string) (Handler, int, bool) { +func match(current *node, path string, start int, params *[]any) (Handler, int, bool) { paramCount := 0 for _, c := range current.children { diff --git a/session/middleware.go b/session/middleware.go index 52b9b37..2272e70 100644 --- a/session/middleware.go +++ b/session/middleware.go @@ -4,7 +4,7 @@ import sushi "git.sharkk.net/Sharkk/Sushi" // Middleware provides session handling func Middleware() sushi.Middleware { - return func(ctx sushi.Ctx, params []string, next func()) { + return func(ctx sushi.Ctx, params []any, next func()) { sessionID := sushi.GetCookie(ctx, SessionCookieName) var sess *Session diff --git a/sushi.go b/sushi.go index bb3876c..64614f3 100644 --- a/sushi.go +++ b/sushi.go @@ -8,7 +8,7 @@ import ( "github.com/valyala/fasthttp" ) -func (h Handler) Serve(ctx Ctx, params []string) { +func (h Handler) Serve(ctx Ctx, params []any) { h(ctx, params) } @@ -20,7 +20,7 @@ func IsHTTPS(ctx Ctx) bool { // StandardHandler adapts a standard fasthttp.RequestHandler to the router's Handler func StandardHandler(handler fasthttp.RequestHandler) Handler { - return func(ctx Ctx, _ []string) { + return func(ctx Ctx, _ []any) { handler(ctx) } } diff --git a/timing/timing.go b/timing/timing.go index 1c668d7..cd82553 100644 --- a/timing/timing.go +++ b/timing/timing.go @@ -11,7 +11,7 @@ const RequestTimerKey = "request_start_time" // Middleware adds request timing functionality func Middleware() sushi.Middleware { - return func(ctx sushi.Ctx, params []string, next func()) { + return func(ctx sushi.Ctx, params []any, next func()) { startTime := time.Now() ctx.SetUserValue(RequestTimerKey, startTime) next() diff --git a/types.go b/types.go index 34eed04..cd08b4a 100644 --- a/types.go +++ b/types.go @@ -3,5 +3,5 @@ package sushi import "github.com/valyala/fasthttp" type Ctx = *fasthttp.RequestCtx -type Handler func(ctx Ctx, params []string) -type Middleware func(ctx Ctx, params []string, next func()) +type Handler func(ctx Ctx, params []any) +type Middleware func(ctx Ctx, params []any, next func())