# 🍣 Sushi
A fast, raw, tasty framework for simplifying basic web apps!
## Quick Start
```go
package main
import "git.sharkk.net/Sharkk/Sushi"
func main() {
app := sushi.New()
// Initialize sessions
sushi.InitSessions("sessions.json")
app.Get("/", func(ctx sushi.Ctx) {
ctx.SendHTML("
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 and Queries
Sushi provides a fluent API for accessing URL parameters, query strings, and form data with automatic type conversion:
### URL Parameters (Named Routes)
```go
app.Get("/users/:id", func(ctx sushi.Ctx) {
// Get URL parameter with fluent API
userID := ctx.Param("id").Int() // /users/123 -> 123
userIDStr := ctx.Param("id").String() // /users/123 -> "123"
// With defaults
limit := ctx.Param("limit").IntDefault(10)
})
app.Get("/users/:id/posts/:slug", func(ctx sushi.Ctx) {
userID := ctx.Param("id").Int()
slug := ctx.Param("slug").String()
// Check if parameter exists
if ctx.Param("optional").Exists() {
// Handle optional parameter
}
})
```
### Query Parameters
```go
app.Get("/search", func(ctx sushi.Ctx) {
// Get query parameters with fluent API
query := ctx.Query("q").String() // ?q=hello -> "hello"
page := ctx.Query("page").IntDefault(1) // ?page=2 -> 2
limit := ctx.Query("limit").IntDefault(20) // Default to 20 if not present
sortBy := ctx.Query("sort").StringDefault("name") // Default to "name"
// Boolean query params
includeDeleted := ctx.Query("deleted").Bool() // ?deleted=true -> true
// Float values
minPrice := ctx.Query("min_price").Float() // ?min_price=19.99 -> 19.99
// Check existence
if ctx.Query("filter").Exists() {
filter := ctx.Query("filter").String()
// Apply filter
}
})
```
## Form Data
Access form data with the fluent API:
```go
app.Post("/users", func(ctx sushi.Ctx) {
// Get form fields with fluent API
name := ctx.Input("name").String()
email := ctx.Input("email").String()
age := ctx.Input("age").IntDefault(0)
// Check if checkbox is checked
agreeToTerms := ctx.Input("terms").Bool() // Checks for "true", "on", "1", "yes"
// Validate required fields
if ctx.Input("email").IsEmpty() {
ctx.SendError(400, "Email is required")
return
}
// Get array of values (for multiple select, checkboxes)
tags := ctx.GetFormArray("tags[]")
})
```
## Response Helpers
```go
func myHandler(ctx sushi.Ctx) {
// JSON responses
ctx.SendJSON(map[string]string{"message": "success"})
// HTML responses
ctx.SendHTML("Welcome
")
// Text responses
ctx.SendText("Plain text")
// Error responses
ctx.SendError(404, "Not Found")
// Redirects
ctx.Redirect("/login")
// Status only
ctx.SendStatus(204)
// File responses
ctx.SendFile("/path/to/file.pdf")
// Raw bytes
ctx.SendBytes(imageData, "image/png")
}
## Middleware
```go
// Custom middleware
func loggingMiddleware() sushi.Middleware {
return func(ctx sushi.Ctx, 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. Password Hashing
The password package supports both Argon2 (default) and Bcrypt algorithms with configurable settings:
```go
import "git.sharkk.net/Sharkk/Sushi/password"
// Using default Argon2 configuration
hashedPassword := password.HashPassword("userpassword123")
// Verify password (auto-detects algorithm)
isValid, err := password.VerifyPassword("userpassword123", hashedPassword)
// Configure for Bcrypt
password.SetConfig(password.Config{
Algorithm: password.Bcrypt,
Bcrypt: password.BcryptConfig{
Cost: 12, // Higher = more secure but slower
},
})
// Configure for Argon2 with custom settings
password.SetConfig(password.Config{
Algorithm: password.Argon2,
Argon2: password.Argon2Config{
Time: 3, // Iterations
Memory: 128 * 1024, // 128 MB
Threads: 4,
KeyLen: 32,
},
})
// Use preset configurations
password.SetConfig(password.Config{
Algorithm: password.Argon2,
Argon2: password.SecureArgon2Config(), // More secure, slower
// Or: password.FastArgon2Config() for development
})
// Check if password needs rehashing (algorithm or params changed)
if password.NeedsRehash(userHashedPassword) {
// Rehash password after successful verification
newHash := password.HashPassword(plainPassword)
// Update stored hash in database
}
```
### 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
sushi.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) {
// Use fluent API for form data
email := ctx.Input("email").String()
password := ctx.Input("password").String()
// Validate inputs
if ctx.Input("email").IsEmpty() || ctx.Input("password").IsEmpty() {
ctx.SendError(400, "Email and password are required")
return
}
// Find user by email/username
user := findUserByEmail(email)
if user == nil {
ctx.SendError(401, "Invalid credentials")
return
}
// Verify password
isValid, err := password.VerifyPassword(password, user.Password)
if err != nil || !isValid {
ctx.SendError(401, "Invalid credentials")
return
}
// Log the user in
ctx.Login(user.ID, user)
ctx.Redirect("/dashboard")
}
```
### 5. Logout Handler
```go
func logoutHandler(ctx sushi.Ctx) {
ctx.Logout()
ctx.Redirect("/")
}
```
### 6. Getting Current User
```go
func dashboardHandler(ctx sushi.Ctx) {
user := ctx.GetCurrentUser().(*User)
html := fmt.Sprintf("Welcome, %s!
", user.Username)
ctx.SendHTML(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) {
csrfField := csrf.CSRFHiddenField(ctx)
html := fmt.Sprintf(`
`, csrfField)
ctx.SendHTML(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) {
sess := ctx.GetCurrentSession()
// 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()
sushi.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) {
if ctx.IsAuthenticated() {
ctx.Redirect("/dashboard")
return
}
ctx.SendHTML(`Login`)
}
func loginPageHandler(ctx sushi.Ctx) {
html := fmt.Sprintf(`
`, csrf.HiddenField(ctx))
ctx.SendHTML(html)
}
func loginHandler(ctx sushi.Ctx) {
// Use fluent API for form data
email := ctx.Input("email").String()
pass := ctx.Input("password").String()
user := findUserByEmail(email)
if user == nil {
ctx.SendError(401, "Invalid credentials")
return
}
if valid, _ := password.VerifyPassword(pass, user.Password); !valid {
ctx.SendError(401, "Invalid credentials")
return
}
ctx.Login(user.ID, user)
ctx.Redirect("/dashboard")
}
func dashboardHandler(ctx sushi.Ctx) {
user := ctx.GetCurrentUser().(*User)
html := fmt.Sprintf(`
Welcome, %s!
`, user.Email, csrf.HiddenField(ctx))
ctx.SendHTML(html)
}
func logoutHandler(ctx sushi.Ctx) {
ctx.Logout()
ctx.Redirect("/")
}
```