Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 828c581899 | |||
| c962f05c81 | |||
| a2c782c54a | |||
| 4bad13510a | |||
| 32116905b7 | |||
| f7d344d3e0 |
173
README.md
173
README.md
@ -15,7 +15,7 @@ func main() {
|
|||||||
// Initialize sessions
|
// Initialize sessions
|
||||||
sushi.InitSessions("sessions.json")
|
sushi.InitSessions("sessions.json")
|
||||||
|
|
||||||
app.Get("/", func(ctx sushi.Ctx, params []any) {
|
app.Get("/", func(ctx sushi.Ctx) {
|
||||||
ctx.SendHTML("<h1>Hello Sushi!</h1>")
|
ctx.SendHTML("<h1>Hello Sushi!</h1>")
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -41,35 +41,86 @@ api.Get("/users", listUsersHandler)
|
|||||||
api.Post("/users", createUserHandler)
|
api.Post("/users", createUserHandler)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parameters
|
## Parameters and Queries
|
||||||
|
|
||||||
URL parameters are automatically converted to the appropriate type:
|
Sushi provides a fluent API for accessing URL parameters, query strings, and form data with automatic type conversion:
|
||||||
|
|
||||||
|
### URL Parameters (Named Routes)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Numeric parameters become integers
|
app.Get("/users/:id", func(ctx sushi.Ctx) {
|
||||||
app.Get("/users/:id", func(ctx sushi.Ctx, params []any) {
|
// Get URL parameter with fluent API
|
||||||
userID := params[0].(int) // /users/123 -> 123
|
userID := ctx.Param("id").Int() // /users/123 -> 123
|
||||||
// ...
|
userIDStr := ctx.Param("id").String() // /users/123 -> "123"
|
||||||
|
|
||||||
|
// With defaults
|
||||||
|
limit := ctx.Param("limit").IntDefault(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
// String parameters stay strings
|
app.Get("/users/:id/posts/:slug", func(ctx sushi.Ctx) {
|
||||||
app.Get("/users/:name", func(ctx sushi.Ctx, params []any) {
|
userID := ctx.Param("id").Int()
|
||||||
name := params[0].(string) // /users/john -> "john"
|
slug := ctx.Param("slug").String()
|
||||||
// ...
|
|
||||||
})
|
|
||||||
|
|
||||||
// Mixed types
|
// Check if parameter exists
|
||||||
app.Get("/users/:id/posts/:slug", func(ctx sushi.Ctx, params []any) {
|
if ctx.Param("optional").Exists() {
|
||||||
userID := params[0].(int) // 123
|
// Handle optional parameter
|
||||||
slug := params[1].(string) // "my-post"
|
}
|
||||||
// ...
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
## Response Helpers
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func myHandler(ctx sushi.Ctx, params []any) {
|
func myHandler(ctx sushi.Ctx) {
|
||||||
// JSON responses
|
// JSON responses
|
||||||
ctx.SendJSON(map[string]string{"message": "success"})
|
ctx.SendJSON(map[string]string{"message": "success"})
|
||||||
|
|
||||||
@ -87,15 +138,20 @@ func myHandler(ctx sushi.Ctx, params []any) {
|
|||||||
|
|
||||||
// Status only
|
// Status only
|
||||||
ctx.SendStatus(204)
|
ctx.SendStatus(204)
|
||||||
|
|
||||||
|
// File responses
|
||||||
|
ctx.SendFile("/path/to/file.pdf")
|
||||||
|
|
||||||
|
// Raw bytes
|
||||||
|
ctx.SendBytes(imageData, "image/png")
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
## Middleware
|
## Middleware
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Custom middleware
|
// Custom middleware
|
||||||
func loggingMiddleware() sushi.Middleware {
|
func loggingMiddleware() sushi.Middleware {
|
||||||
return func(ctx sushi.Ctx, params []any, next func()) {
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
println("Request:", string(ctx.Method()), string(ctx.Path()))
|
println("Request:", string(ctx.Method()), string(ctx.Path()))
|
||||||
next()
|
next()
|
||||||
println("Status:", ctx.Response.StatusCode())
|
println("Status:", ctx.Response.StatusCode())
|
||||||
@ -111,16 +167,51 @@ admin.Use(auth.RequireAuth("/login"))
|
|||||||
|
|
||||||
## Authentication Workflow
|
## Authentication Workflow
|
||||||
|
|
||||||
### 1. Setup Password Hashing
|
### 1. Password Hashing
|
||||||
|
|
||||||
|
The password package supports both Argon2 (default) and Bcrypt algorithms with configurable settings:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "git.sharkk.net/Sharkk/Sushi/password"
|
import "git.sharkk.net/Sharkk/Sushi/password"
|
||||||
|
|
||||||
// Hash password for storage
|
// Using default Argon2 configuration
|
||||||
hashedPassword := password.HashPassword("userpassword123")
|
hashedPassword := password.HashPassword("userpassword123")
|
||||||
|
|
||||||
// Verify password during login
|
// Verify password (auto-detects algorithm)
|
||||||
isValid, err := password.VerifyPassword("userpassword123", hashedPassword)
|
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
|
### 2. User Structure
|
||||||
@ -176,9 +267,16 @@ func main() {
|
|||||||
### 4. Login Handler
|
### 4. Login Handler
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func loginHandler(ctx sushi.Ctx, params []any) {
|
func loginHandler(ctx sushi.Ctx) {
|
||||||
email := string(ctx.PostArgs().Peek("email"))
|
// Use fluent API for form data
|
||||||
password := string(ctx.PostArgs().Peek("password"))
|
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
|
// Find user by email/username
|
||||||
user := findUserByEmail(email)
|
user := findUserByEmail(email)
|
||||||
@ -204,7 +302,7 @@ func loginHandler(ctx sushi.Ctx, params []any) {
|
|||||||
### 5. Logout Handler
|
### 5. Logout Handler
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func logoutHandler(ctx sushi.Ctx, params []any) {
|
func logoutHandler(ctx sushi.Ctx) {
|
||||||
ctx.Logout()
|
ctx.Logout()
|
||||||
ctx.Redirect("/")
|
ctx.Redirect("/")
|
||||||
}
|
}
|
||||||
@ -213,7 +311,7 @@ func logoutHandler(ctx sushi.Ctx, params []any) {
|
|||||||
### 6. Getting Current User
|
### 6. Getting Current User
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func dashboardHandler(ctx sushi.Ctx, params []any) {
|
func dashboardHandler(ctx sushi.Ctx) {
|
||||||
user := ctx.GetCurrentUser().(*User)
|
user := ctx.GetCurrentUser().(*User)
|
||||||
|
|
||||||
html := fmt.Sprintf("<h1>Welcome, %s!</h1>", user.Username)
|
html := fmt.Sprintf("<h1>Welcome, %s!</h1>", user.Username)
|
||||||
@ -230,7 +328,7 @@ import "git.sharkk.net/Sharkk/Sushi/csrf"
|
|||||||
app.Use(csrf.Middleware())
|
app.Use(csrf.Middleware())
|
||||||
|
|
||||||
// In your form template
|
// In your form template
|
||||||
func loginPageHandler(ctx sushi.Ctx, params []any) {
|
func loginPageHandler(ctx sushi.Ctx) {
|
||||||
csrfField := csrf.CSRFHiddenField(ctx)
|
csrfField := csrf.CSRFHiddenField(ctx)
|
||||||
|
|
||||||
html := fmt.Sprintf(`
|
html := fmt.Sprintf(`
|
||||||
@ -266,7 +364,7 @@ app.Get("/assets/*path", sushi.StaticEmbed(files))
|
|||||||
## Sessions
|
## Sessions
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func someHandler(ctx sushi.Ctx, params []any) {
|
func someHandler(ctx sushi.Ctx) {
|
||||||
sess := ctx.GetCurrentSession()
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
// Set session data
|
// Set session data
|
||||||
@ -357,7 +455,7 @@ func main() {
|
|||||||
app.Listen(":8080")
|
app.Listen(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
func homeHandler(ctx sushi.Ctx, params []any) {
|
func homeHandler(ctx sushi.Ctx) {
|
||||||
if ctx.IsAuthenticated() {
|
if ctx.IsAuthenticated() {
|
||||||
ctx.Redirect("/dashboard")
|
ctx.Redirect("/dashboard")
|
||||||
return
|
return
|
||||||
@ -365,7 +463,7 @@ func homeHandler(ctx sushi.Ctx, params []any) {
|
|||||||
ctx.SendHTML(`<a href="/login">Login</a>`)
|
ctx.SendHTML(`<a href="/login">Login</a>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginPageHandler(ctx sushi.Ctx, params []any) {
|
func loginPageHandler(ctx sushi.Ctx) {
|
||||||
html := fmt.Sprintf(`
|
html := fmt.Sprintf(`
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/login">
|
||||||
%s
|
%s
|
||||||
@ -378,9 +476,10 @@ func loginPageHandler(ctx sushi.Ctx, params []any) {
|
|||||||
ctx.SendHTML(html)
|
ctx.SendHTML(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginHandler(ctx sushi.Ctx, params []any) {
|
func loginHandler(ctx sushi.Ctx) {
|
||||||
email := string(ctx.PostArgs().Peek("email"))
|
// Use fluent API for form data
|
||||||
pass := string(ctx.PostArgs().Peek("password"))
|
email := ctx.Input("email").String()
|
||||||
|
pass := ctx.Input("password").String()
|
||||||
|
|
||||||
user := findUserByEmail(email)
|
user := findUserByEmail(email)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
@ -397,7 +496,7 @@ func loginHandler(ctx sushi.Ctx, params []any) {
|
|||||||
ctx.Redirect("/dashboard")
|
ctx.Redirect("/dashboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
func dashboardHandler(ctx sushi.Ctx, params []any) {
|
func dashboardHandler(ctx sushi.Ctx) {
|
||||||
user := ctx.GetCurrentUser().(*User)
|
user := ctx.GetCurrentUser().(*User)
|
||||||
|
|
||||||
html := fmt.Sprintf(`
|
html := fmt.Sprintf(`
|
||||||
@ -411,7 +510,7 @@ func dashboardHandler(ctx sushi.Ctx, params []any) {
|
|||||||
ctx.SendHTML(html)
|
ctx.SendHTML(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutHandler(ctx sushi.Ctx, params []any) {
|
func logoutHandler(ctx sushi.Ctx) {
|
||||||
ctx.Logout()
|
ctx.Logout()
|
||||||
ctx.Redirect("/")
|
ctx.Redirect("/")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,22 @@ import (
|
|||||||
|
|
||||||
const UserCtxKey = "user"
|
const UserCtxKey = "user"
|
||||||
|
|
||||||
// Middleware adds authentication handling
|
// Auth holds the authentication middleware and user lookup function
|
||||||
func Middleware(userLookup func(int) any) sushi.Middleware {
|
type Auth struct {
|
||||||
return func(ctx sushi.Ctx, params []any, next func()) {
|
userLookup func(int) any
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Auth instance
|
||||||
|
func New(userLookup func(int) any) *Auth {
|
||||||
|
return &Auth{userLookup: userLookup}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware returns the authentication middleware function
|
||||||
|
func (a *Auth) Middleware() sushi.Middleware {
|
||||||
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
sess := sushi.GetCurrentSession(ctx)
|
sess := sushi.GetCurrentSession(ctx)
|
||||||
if sess != nil && sess.UserID > 0 && userLookup != nil {
|
if sess != nil && sess.UserID > 0 && a.userLookup != nil {
|
||||||
user := userLookup(sess.UserID)
|
user := a.userLookup(sess.UserID)
|
||||||
if user != nil {
|
if user != nil {
|
||||||
ctx.SetUserValue(UserCtxKey, user)
|
ctx.SetUserValue(UserCtxKey, user)
|
||||||
} else {
|
} else {
|
||||||
@ -24,6 +34,15 @@ func Middleware(userLookup func(int) any) sushi.Middleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update refreshes the current user data in the context
|
||||||
|
func (a *Auth) Update(ctx sushi.Ctx) {
|
||||||
|
sess := sushi.GetCurrentSession(ctx)
|
||||||
|
if sess != nil && sess.UserID > 0 && a.userLookup != nil {
|
||||||
|
user := a.userLookup(sess.UserID)
|
||||||
|
ctx.SetUserValue(UserCtxKey, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RequireAuth middleware that redirects unauthenticated users
|
// RequireAuth middleware that redirects unauthenticated users
|
||||||
func RequireAuth(redirectPath ...string) sushi.Middleware {
|
func RequireAuth(redirectPath ...string) sushi.Middleware {
|
||||||
redirect := "/login"
|
redirect := "/login"
|
||||||
@ -31,7 +50,7 @@ func RequireAuth(redirectPath ...string) sushi.Middleware {
|
|||||||
redirect = redirectPath[0]
|
redirect = redirectPath[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(ctx sushi.Ctx, params []any, next func()) {
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
if !ctx.IsAuthenticated() {
|
if !ctx.IsAuthenticated() {
|
||||||
ctx.Redirect(redirect, fasthttp.StatusFound)
|
ctx.Redirect(redirect, fasthttp.StatusFound)
|
||||||
return
|
return
|
||||||
@ -47,7 +66,7 @@ func RequireGuest(redirectPath ...string) sushi.Middleware {
|
|||||||
redirect = redirectPath[0]
|
redirect = redirectPath[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(ctx sushi.Ctx, params []any, next func()) {
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
if ctx.IsAuthenticated() {
|
if ctx.IsAuthenticated() {
|
||||||
ctx.Redirect(redirect, fasthttp.StatusFound)
|
ctx.Redirect(redirect, fasthttp.StatusFound)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -110,7 +110,7 @@ func ValidateFormCSRFToken(ctx sushi.Ctx) bool {
|
|||||||
|
|
||||||
// Middleware returns middleware that automatically validates CSRF tokens
|
// Middleware returns middleware that automatically validates CSRF tokens
|
||||||
func Middleware() sushi.Middleware {
|
func Middleware() sushi.Middleware {
|
||||||
return func(ctx sushi.Ctx, params []any, next func()) {
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
method := string(ctx.Method())
|
method := string(ctx.Method())
|
||||||
|
|
||||||
if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" {
|
if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" {
|
||||||
|
|||||||
13
forms.go
13
forms.go
@ -10,13 +10,18 @@ type FormValue struct {
|
|||||||
exists bool
|
exists bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Form gets a form field for chaining
|
// Input gets a form field for chaining
|
||||||
func (ctx Ctx) Form(key string) FormValue {
|
func (ctx Ctx) Input(key string) FormValue {
|
||||||
value := string(ctx.PostArgs().Peek(key))
|
value := string(ctx.PostArgs().Peek(key))
|
||||||
exists := ctx.PostArgs().Has(key)
|
exists := ctx.PostArgs().Has(key)
|
||||||
return FormValue{value: value, exists: exists}
|
return FormValue{value: value, exists: exists}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Form gets a form field for chaining (deprecated: use Input instead)
|
||||||
|
func (ctx Ctx) Form(key string) FormValue {
|
||||||
|
return ctx.Input(key)
|
||||||
|
}
|
||||||
|
|
||||||
// String returns the value as string
|
// String returns the value as string
|
||||||
func (f FormValue) String() string {
|
func (f FormValue) String() string {
|
||||||
return f.value
|
return f.value
|
||||||
@ -101,10 +106,10 @@ func (f FormValue) IsEmpty() bool {
|
|||||||
// GetFormArray gets multiple form values as string slice
|
// GetFormArray gets multiple form values as string slice
|
||||||
func (ctx Ctx) GetFormArray(key string) []string {
|
func (ctx Ctx) GetFormArray(key string) []string {
|
||||||
var values []string
|
var values []string
|
||||||
ctx.PostArgs().VisitAll(func(k, v []byte) {
|
for k, v := range ctx.PostArgs().All() {
|
||||||
if string(k) == key {
|
if string(k) == key {
|
||||||
values = append(values, string(v))
|
values = append(values, string(v))
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|||||||
6
fs.go
6
fs.go
@ -50,7 +50,7 @@ func StaticFS(fsOptions StaticOptions) Handler {
|
|||||||
|
|
||||||
fsHandler := fs.NewRequestHandler()
|
fsHandler := fs.NewRequestHandler()
|
||||||
|
|
||||||
return func(ctx Ctx, params []any) {
|
return func(ctx Ctx) {
|
||||||
fsHandler(ctx.RequestCtx)
|
fsHandler(ctx.RequestCtx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,14 +62,14 @@ func Static(root string) Handler {
|
|||||||
|
|
||||||
// StaticFile serves a single file
|
// StaticFile serves a single file
|
||||||
func StaticFile(filePath string) Handler {
|
func StaticFile(filePath string) Handler {
|
||||||
return func(ctx Ctx, params []any) {
|
return func(ctx Ctx) {
|
||||||
fasthttp.ServeFile(ctx.RequestCtx, filePath)
|
fasthttp.ServeFile(ctx.RequestCtx, filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticEmbed creates a handler for embedded files
|
// StaticEmbed creates a handler for embedded files
|
||||||
func StaticEmbed(files map[string][]byte) Handler {
|
func StaticEmbed(files map[string][]byte) Handler {
|
||||||
return func(ctx Ctx, params []any) {
|
return func(ctx Ctx) {
|
||||||
requestPath := string(ctx.Path())
|
requestPath := string(ctx.Path())
|
||||||
|
|
||||||
// Try to find the file
|
// Try to find the file
|
||||||
|
|||||||
110
params.go
Normal file
110
params.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package sushi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type ParamValue struct {
|
||||||
|
value string
|
||||||
|
exists bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param gets a route parameter by name for chaining (requires named params)
|
||||||
|
func (ctx Ctx) Param(name string) ParamValue {
|
||||||
|
if paramMap, ok := ctx.UserValue("param_names").(map[string]string); ok {
|
||||||
|
if value, exists := paramMap[name]; exists {
|
||||||
|
return ParamValue{value: value, exists: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ParamValue{value: "", exists: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query gets a query parameter for chaining
|
||||||
|
func (ctx Ctx) Query(key string) ParamValue {
|
||||||
|
value := string(ctx.QueryArgs().Peek(key))
|
||||||
|
exists := ctx.QueryArgs().Has(key)
|
||||||
|
return ParamValue{value: value, exists: exists}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the value as string
|
||||||
|
func (p ParamValue) String() string {
|
||||||
|
return p.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringDefault returns string with default value
|
||||||
|
func (p ParamValue) StringDefault(defaultValue string) string {
|
||||||
|
if p.value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return p.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns the value as integer
|
||||||
|
func (p ParamValue) Int() int {
|
||||||
|
if p.value == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if parsed, err := strconv.Atoi(p.value); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntDefault returns integer with default value
|
||||||
|
func (p ParamValue) IntDefault(defaultValue int) int {
|
||||||
|
if p.value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
if parsed, err := strconv.Atoi(p.value); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float returns the value as float64
|
||||||
|
func (p ParamValue) Float() float64 {
|
||||||
|
if p.value == "" {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
if parsed, err := strconv.ParseFloat(p.value, 64); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FloatDefault returns float64 with default value
|
||||||
|
func (p ParamValue) FloatDefault(defaultValue float64) float64 {
|
||||||
|
if p.value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
if parsed, err := strconv.ParseFloat(p.value, 64); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns the value as boolean
|
||||||
|
func (p ParamValue) Bool() bool {
|
||||||
|
value := strings.ToLower(p.value)
|
||||||
|
return value == "true" || value == "on" || value == "1" || value == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolDefault returns boolean with default value
|
||||||
|
func (p ParamValue) BoolDefault(defaultValue bool) bool {
|
||||||
|
if p.value == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return p.Bool()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists returns true if the parameter exists
|
||||||
|
func (p ParamValue) Exists() bool {
|
||||||
|
return p.exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the parameter is empty or doesn't exist
|
||||||
|
func (p ParamValue) IsEmpty() bool {
|
||||||
|
return p.value == ""
|
||||||
|
}
|
||||||
@ -8,33 +8,158 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Algorithm represents the hashing algorithm to use
|
||||||
|
type Algorithm int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
argonTime = 1
|
Argon2 Algorithm = iota
|
||||||
argonMemory = 64 * 1024
|
Bcrypt
|
||||||
argonThreads = 4
|
|
||||||
argonKeyLen = 32
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HashPassword creates an argon2id hash of the password
|
// Argon2Config holds configuration for Argon2 hashing
|
||||||
|
type Argon2Config struct {
|
||||||
|
Time uint32 // Number of iterations
|
||||||
|
Memory uint32 // Memory usage in KB
|
||||||
|
Threads uint8 // Number of threads
|
||||||
|
KeyLen uint32 // Length of generated hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// BcryptConfig holds configuration for Bcrypt hashing
|
||||||
|
type BcryptConfig struct {
|
||||||
|
Cost int // Bcrypt cost factor (4-31)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds the password hashing configuration
|
||||||
|
type Config struct {
|
||||||
|
Algorithm Algorithm
|
||||||
|
Argon2 Argon2Config
|
||||||
|
Bcrypt BcryptConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultArgon2Config returns recommended Argon2 settings
|
||||||
|
func DefaultArgon2Config() Argon2Config {
|
||||||
|
return Argon2Config{
|
||||||
|
Time: 1,
|
||||||
|
Memory: 64 * 1024, // 64 MB
|
||||||
|
Threads: 4,
|
||||||
|
KeyLen: 32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureArgon2Config returns more secure Argon2 settings (slower but more resistant)
|
||||||
|
func SecureArgon2Config() Argon2Config {
|
||||||
|
return Argon2Config{
|
||||||
|
Time: 3,
|
||||||
|
Memory: 128 * 1024, // 128 MB
|
||||||
|
Threads: 4,
|
||||||
|
KeyLen: 32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FastArgon2Config returns faster Argon2 settings (for testing/development)
|
||||||
|
func FastArgon2Config() Argon2Config {
|
||||||
|
return Argon2Config{
|
||||||
|
Time: 1,
|
||||||
|
Memory: 32 * 1024, // 32 MB
|
||||||
|
Threads: 2,
|
||||||
|
KeyLen: 32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultBcryptConfig returns recommended Bcrypt settings
|
||||||
|
func DefaultBcryptConfig() BcryptConfig {
|
||||||
|
return BcryptConfig{
|
||||||
|
Cost: bcrypt.DefaultCost, // 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecureBcryptConfig returns more secure Bcrypt settings (slower but more resistant)
|
||||||
|
func SecureBcryptConfig() BcryptConfig {
|
||||||
|
return BcryptConfig{
|
||||||
|
Cost: 12,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns the default configuration (Argon2 with default settings)
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Algorithm: Argon2,
|
||||||
|
Argon2: DefaultArgon2Config(),
|
||||||
|
Bcrypt: DefaultBcryptConfig(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalConfig = DefaultConfig()
|
||||||
|
|
||||||
|
// SetConfig sets the global password configuration
|
||||||
|
func SetConfig(config Config) {
|
||||||
|
globalConfig = config
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig returns the current global password configuration
|
||||||
|
func GetConfig() Config {
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword creates a hash of the password using the global configuration
|
||||||
func HashPassword(password string) string {
|
func HashPassword(password string) string {
|
||||||
|
return HashPasswordWithConfig(password, globalConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPasswordWithConfig creates a hash of the password using the specified configuration
|
||||||
|
func HashPasswordWithConfig(password string, config Config) string {
|
||||||
|
switch config.Algorithm {
|
||||||
|
case Bcrypt:
|
||||||
|
return hashBcrypt(password, config.Bcrypt)
|
||||||
|
case Argon2:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
return hashArgon2(password, config.Argon2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashArgon2 creates an argon2id hash of the password
|
||||||
|
func hashArgon2(password string, config Argon2Config) string {
|
||||||
salt := make([]byte, 16)
|
salt := make([]byte, 16)
|
||||||
rand.Read(salt)
|
rand.Read(salt)
|
||||||
|
|
||||||
hash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
|
hash := argon2.IDKey([]byte(password), salt, config.Time, config.Memory, config.Threads, config.KeyLen)
|
||||||
|
|
||||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||||
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
||||||
|
|
||||||
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||||
argon2.Version, argonMemory, argonTime, argonThreads, b64Salt, b64Hash)
|
argon2.Version, config.Memory, config.Time, config.Threads, b64Salt, b64Hash)
|
||||||
|
|
||||||
return encoded
|
return encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hashBcrypt creates a bcrypt hash of the password
|
||||||
|
func hashBcrypt(password string, config BcryptConfig) string {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), config.Cost)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to default cost if provided cost is invalid
|
||||||
|
hash, _ = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
}
|
||||||
|
return string(hash)
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyPassword checks if a password matches the hash
|
// VerifyPassword checks if a password matches the hash
|
||||||
func VerifyPassword(password, encodedHash string) (bool, error) {
|
func VerifyPassword(password, encodedHash string) (bool, error) {
|
||||||
|
// Detect hash type by prefix
|
||||||
|
if strings.HasPrefix(encodedHash, "$2a$") || strings.HasPrefix(encodedHash, "$2b$") || strings.HasPrefix(encodedHash, "$2y$") {
|
||||||
|
return verifyBcrypt(password, encodedHash)
|
||||||
|
} else if strings.HasPrefix(encodedHash, "$argon2id$") {
|
||||||
|
return verifyArgon2(password, encodedHash)
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("unsupported hash format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyArgon2 checks if a password matches an argon2 hash
|
||||||
|
func verifyArgon2(password, encodedHash string) (bool, error) {
|
||||||
parts := strings.Split(encodedHash, "$")
|
parts := strings.Split(encodedHash, "$")
|
||||||
if len(parts) != 6 {
|
if len(parts) != 6 {
|
||||||
return false, fmt.Errorf("invalid hash format")
|
return false, fmt.Errorf("invalid hash format")
|
||||||
@ -77,3 +202,52 @@ func VerifyPassword(password, encodedHash string) (bool, error) {
|
|||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verifyBcrypt checks if a password matches a bcrypt hash
|
||||||
|
func verifyBcrypt(password, encodedHash string) (bool, error) {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(encodedHash), []byte(password))
|
||||||
|
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeedsRehash checks if a hash needs to be updated to current configuration
|
||||||
|
func NeedsRehash(encodedHash string) bool {
|
||||||
|
// Check if using bcrypt when we want argon2
|
||||||
|
if globalConfig.Algorithm == Argon2 && (strings.HasPrefix(encodedHash, "$2a$") || strings.HasPrefix(encodedHash, "$2b$") || strings.HasPrefix(encodedHash, "$2y$")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if using argon2 when we want bcrypt
|
||||||
|
if globalConfig.Algorithm == Bcrypt && strings.HasPrefix(encodedHash, "$argon2id$") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// For bcrypt, check if cost has changed
|
||||||
|
if strings.HasPrefix(encodedHash, "$2") {
|
||||||
|
cost, err := bcrypt.Cost([]byte(encodedHash))
|
||||||
|
if err == nil && cost != globalConfig.Bcrypt.Cost {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For argon2, check if parameters have changed
|
||||||
|
if strings.HasPrefix(encodedHash, "$argon2id$") {
|
||||||
|
parts := strings.Split(encodedHash, "$")
|
||||||
|
if len(parts) == 6 {
|
||||||
|
var m, t, p uint32
|
||||||
|
_, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p)
|
||||||
|
if err == nil {
|
||||||
|
if m != globalConfig.Argon2.Memory || t != globalConfig.Argon2.Time || p != uint32(globalConfig.Argon2.Threads) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
74
router.go
74
router.go
@ -12,6 +12,7 @@ type node struct {
|
|||||||
children []*node
|
children []*node
|
||||||
isDynamic bool
|
isDynamic bool
|
||||||
isWildcard bool
|
isWildcard bool
|
||||||
|
paramNames []string
|
||||||
maxParams uint8
|
maxParams uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ type Router struct {
|
|||||||
patch *node
|
patch *node
|
||||||
delete *node
|
delete *node
|
||||||
middleware []Middleware
|
middleware []Middleware
|
||||||
paramsBuffer []any
|
paramsBuffer []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
@ -40,7 +41,7 @@ func NewRouter() *Router {
|
|||||||
patch: &node{},
|
patch: &node{},
|
||||||
delete: &node{},
|
delete: &node{},
|
||||||
middleware: []Middleware{},
|
middleware: []Middleware{},
|
||||||
paramsBuffer: make([]any, 64),
|
paramsBuffer: make([]string, 64),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,13 +50,30 @@ func (r *Router) ServeHTTP(ctx *fasthttp.RequestCtx) {
|
|||||||
path := string(ctx.Path())
|
path := string(ctx.Path())
|
||||||
method := string(ctx.Method())
|
method := string(ctx.Method())
|
||||||
|
|
||||||
h, params, found := r.Lookup(method, path)
|
h, params, paramNames, found := r.Lookup(method, path)
|
||||||
if !found {
|
if !found {
|
||||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h(Ctx{ctx}, params)
|
// Store params in context
|
||||||
|
sushiCtx := Ctx{RequestCtx: ctx, Params: params}
|
||||||
|
if len(params) > 0 {
|
||||||
|
// Create named param map if param names exist
|
||||||
|
if len(paramNames) > 0 {
|
||||||
|
paramMap := make(map[string]string)
|
||||||
|
for i, name := range paramNames {
|
||||||
|
if i < len(params) && name != "" {
|
||||||
|
paramMap[name] = params[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(paramMap) > 0 {
|
||||||
|
sushiCtx.SetUserValue("param_names", paramMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h(sushiCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler returns a fasthttp request handler
|
// Handler returns a fasthttp request handler
|
||||||
@ -145,19 +163,19 @@ func applyMiddleware(h Handler, mw []Middleware) Handler {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(ctx Ctx, params []any) {
|
return func(ctx Ctx) {
|
||||||
var index int
|
var index int
|
||||||
var next func()
|
var next func()
|
||||||
|
|
||||||
next = func() {
|
next = func() {
|
||||||
if index >= len(mw) {
|
if index >= len(mw) {
|
||||||
h(ctx, params)
|
h(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currentMW := mw[index]
|
currentMW := mw[index]
|
||||||
index++
|
index++
|
||||||
currentMW(ctx, params, next)
|
currentMW(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
@ -181,6 +199,16 @@ func readSegment(path string, start int) (segment string, end int, hasMore bool)
|
|||||||
return path[start:end], end, end < len(path)
|
return path[start:end], end, end < len(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractParamName(segment string) string {
|
||||||
|
if len(segment) > 1 && segment[0] == ':' {
|
||||||
|
return segment[1:]
|
||||||
|
}
|
||||||
|
if len(segment) > 1 && segment[0] == '*' {
|
||||||
|
return segment[1:]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) error {
|
func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) error {
|
||||||
h = applyMiddleware(h, mw)
|
h = applyMiddleware(h, mw)
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
@ -191,6 +219,8 @@ func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) e
|
|||||||
pos := 0
|
pos := 0
|
||||||
lastWC := false
|
lastWC := false
|
||||||
count := uint8(0)
|
count := uint8(0)
|
||||||
|
var paramNames []string
|
||||||
|
|
||||||
for {
|
for {
|
||||||
seg, newPos, more := readSegment(path, pos)
|
seg, newPos, more := readSegment(path, pos)
|
||||||
if seg == "" {
|
if seg == "" {
|
||||||
@ -206,6 +236,7 @@ func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) e
|
|||||||
}
|
}
|
||||||
if isDyn || isWC {
|
if isDyn || isWC {
|
||||||
count++
|
count++
|
||||||
|
paramNames = append(paramNames, extractParamName(seg))
|
||||||
}
|
}
|
||||||
var child *node
|
var child *node
|
||||||
for _, c := range current.children {
|
for _, c := range current.children {
|
||||||
@ -225,35 +256,36 @@ func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) e
|
|||||||
pos = newPos
|
pos = newPos
|
||||||
}
|
}
|
||||||
current.handler = h
|
current.handler = h
|
||||||
|
current.paramNames = paramNames
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup finds a handler matching method and path
|
// Lookup finds a handler matching method and path
|
||||||
func (r *Router) Lookup(method, path string) (Handler, []any, bool) {
|
func (r *Router) Lookup(method, path string) (Handler, []string, []string, bool) {
|
||||||
root := r.methodNode(method)
|
root := r.methodNode(method)
|
||||||
if root == nil {
|
if root == nil {
|
||||||
return nil, nil, false
|
return nil, nil, nil, false
|
||||||
}
|
}
|
||||||
if path == "/" {
|
if path == "/" {
|
||||||
return root.handler, nil, root.handler != nil
|
return root.handler, nil, nil, root.handler != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := r.paramsBuffer
|
buffer := r.paramsBuffer
|
||||||
if cap(buffer) < int(root.maxParams) {
|
if cap(buffer) < int(root.maxParams) {
|
||||||
buffer = make([]any, root.maxParams)
|
buffer = make([]string, root.maxParams)
|
||||||
r.paramsBuffer = buffer
|
r.paramsBuffer = buffer
|
||||||
}
|
}
|
||||||
buffer = buffer[:0]
|
buffer = buffer[:0]
|
||||||
|
|
||||||
h, paramCount, found := match(root, path, 0, &buffer)
|
h, paramCount, paramNames, found := match(root, path, 0, &buffer)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, nil, false
|
return nil, nil, nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return h, buffer[:paramCount], true
|
return h, buffer[:paramCount], paramNames, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func match(current *node, path string, start int, params *[]any) (Handler, int, bool) {
|
func match(current *node, path string, start int, params *[]string) (Handler, int, []string, bool) {
|
||||||
paramCount := 0
|
paramCount := 0
|
||||||
|
|
||||||
for _, c := range current.children {
|
for _, c := range current.children {
|
||||||
@ -263,13 +295,13 @@ func match(current *node, path string, start int, params *[]any) (Handler, int,
|
|||||||
rem = rem[1:]
|
rem = rem[1:]
|
||||||
}
|
}
|
||||||
*params = append(*params, rem)
|
*params = append(*params, rem)
|
||||||
return c.handler, 1, c.handler != nil
|
return c.handler, 1, c.paramNames, c.handler != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
seg, pos, more := readSegment(path, start)
|
seg, pos, more := readSegment(path, start)
|
||||||
if seg == "" {
|
if seg == "" {
|
||||||
return current.handler, 0, current.handler != nil
|
return current.handler, 0, current.paramNames, current.handler != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range current.children {
|
for _, c := range current.children {
|
||||||
@ -279,14 +311,14 @@ func match(current *node, path string, start int, params *[]any) (Handler, int,
|
|||||||
paramCount++
|
paramCount++
|
||||||
}
|
}
|
||||||
if !more {
|
if !more {
|
||||||
return c.handler, paramCount, c.handler != nil
|
return c.handler, paramCount, c.paramNames, c.handler != nil
|
||||||
}
|
}
|
||||||
h, nestedCount, ok := match(c, path, pos, params)
|
h, nestedCount, paramNames, ok := match(c, path, pos, params)
|
||||||
if ok {
|
if ok {
|
||||||
return h, paramCount + nestedCount, true
|
return h, paramCount + nestedCount, paramNames, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, 0, false
|
return nil, 0, nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,6 +104,10 @@ func (s *Session) GetFlash(key string) (any, bool) {
|
|||||||
return value, exists
|
return value, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Session) DeleteFlash(key string) {
|
||||||
|
s.Delete("flash_" + key)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) GetFlashMessage(key string) string {
|
func (s *Session) GetFlashMessage(key string) string {
|
||||||
if flash, exists := s.GetFlash(key); exists {
|
if flash, exists := s.GetFlash(key); exists {
|
||||||
if msg, ok := flash.(string); ok {
|
if msg, ok := flash.(string); ok {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import sushi "git.sharkk.net/Sharkk/Sushi"
|
|||||||
|
|
||||||
// Middleware provides session handling
|
// Middleware provides session handling
|
||||||
func Middleware() sushi.Middleware {
|
func Middleware() sushi.Middleware {
|
||||||
return func(ctx sushi.Ctx, params []any, next func()) {
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
sessionID := sushi.GetCookie(ctx, sushi.SessionCookieName)
|
sessionID := sushi.GetCookie(ctx, sushi.SessionCookieName)
|
||||||
var sess *sushi.Session
|
var sess *sushi.Session
|
||||||
|
|
||||||
|
|||||||
6
sushi.go
6
sushi.go
@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h Handler) Serve(ctx Ctx, params []any) {
|
func (h Handler) Serve(ctx Ctx) {
|
||||||
h(ctx, params)
|
h(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsHTTPS(ctx Ctx) bool {
|
func IsHTTPS(ctx Ctx) bool {
|
||||||
@ -20,7 +20,7 @@ func IsHTTPS(ctx Ctx) bool {
|
|||||||
|
|
||||||
// StandardHandler adapts a standard fasthttp.RequestHandler to the router's Handler
|
// StandardHandler adapts a standard fasthttp.RequestHandler to the router's Handler
|
||||||
func StandardHandler(handler fasthttp.RequestHandler) Handler {
|
func StandardHandler(handler fasthttp.RequestHandler) Handler {
|
||||||
return func(ctx Ctx, _ []any) {
|
return func(ctx Ctx) {
|
||||||
handler(ctx.RequestCtx)
|
handler(ctx.RequestCtx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const RequestTimerKey = "request_start_time"
|
|||||||
|
|
||||||
// Middleware adds request timing functionality
|
// Middleware adds request timing functionality
|
||||||
func Middleware() sushi.Middleware {
|
func Middleware() sushi.Middleware {
|
||||||
return func(ctx sushi.Ctx, params []any, next func()) {
|
return func(ctx sushi.Ctx, next func()) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
ctx.SetUserValue(RequestTimerKey, startTime)
|
ctx.SetUserValue(RequestTimerKey, startTime)
|
||||||
next()
|
next()
|
||||||
|
|||||||
5
types.go
5
types.go
@ -8,10 +8,11 @@ import (
|
|||||||
|
|
||||||
type Ctx struct {
|
type Ctx struct {
|
||||||
*fasthttp.RequestCtx
|
*fasthttp.RequestCtx
|
||||||
|
Params []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler func(ctx Ctx, params []any)
|
type Handler func(ctx Ctx)
|
||||||
type Middleware func(ctx Ctx, params []any, next func())
|
type Middleware func(ctx Ctx, next func())
|
||||||
|
|
||||||
// SendHTML sends an HTML response
|
// SendHTML sends an HTML response
|
||||||
func (ctx Ctx) SendHTML(html string) {
|
func (ctx Ctx) SendHTML(html string) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user