Skip to content

Commit 188aad2

Browse files
committed
Merge branch 'main' of github.com:xraph/forge
2 parents d7dccb3 + e54fcb3 commit 188aad2

52 files changed

Lines changed: 2524 additions & 119 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## [0.9.12](https://github.com/xraph/forge/compare/v0.9.11...v0.9.12) (2026-02-18)
4+
5+
6+
### Maintenance
7+
8+
* **deps:** remove gorilla/websocket dependency from go.mod and go.sum ([3457e7c](https://github.com/xraph/forge/commit/3457e7c))
9+
* **changelog:** update CHANGELOG.md for v0.9.11 ([39f418c](https://github.com/xraph/forge/commit/39f418c))
10+
* add .worktrees/ to gitignore ([ed4ad90](https://github.com/xraph/forge/commit/ed4ad90))
11+
312
## [0.9.11](https://github.com/xraph/forge/compare/v0.9.10...v0.9.11) (2026-02-18)
413

514

extensions/dashboard/auth/context.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ func WithUser(ctx context.Context, user *UserInfo) context.Context {
1515
// Returns nil if no user is stored (i.e. unauthenticated request).
1616
func UserFromContext(ctx context.Context) *UserInfo {
1717
user, _ := ctx.Value(userContextKey).(*UserInfo)
18+
1819
return user
1920
}
2021

2122
// IsAuthenticated returns true if the context contains an authenticated user.
2223
func IsAuthenticated(ctx context.Context) bool {
2324
user := UserFromContext(ctx)
25+
2426
return user.Authenticated()
2527
}

extensions/dashboard/auth/middleware.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func PageMiddleware(level AccessLevel, loginPath string) router.Middleware {
6565

6666
// Unauthenticated — redirect to login
6767
handleUnauthorized(ctx, loginPath)
68+
6869
return nil, nil
6970

7071
default:
@@ -83,11 +84,13 @@ func RequireRole(role string) router.Middleware {
8384
user := UserFromContext(ctx.Context())
8485
if !user.Authenticated() {
8586
ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
87+
8688
return g.Text("401 - Unauthorized"), nil
8789
}
8890

8991
if !user.HasRole(role) {
9092
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
93+
9194
return html.Div(
9295
html.Class("p-6 text-center"),
9396
html.H2(html.Class("text-xl font-semibold text-destructive mb-2"), g.Text("Access Denied")),
@@ -107,11 +110,13 @@ func RequireScope(scope string) router.Middleware {
107110
user := UserFromContext(ctx.Context())
108111
if !user.Authenticated() {
109112
ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
113+
110114
return g.Text("401 - Unauthorized"), nil
111115
}
112116

113117
if !user.HasScope(scope) {
114118
ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
119+
115120
return html.Div(
116121
html.Class("p-6 text-center"),
117122
html.H2(html.Class("text-xl font-semibold text-destructive mb-2"), g.Text("Access Denied")),
@@ -130,17 +135,19 @@ func RequireScope(scope string) router.Middleware {
130135
// For normal requests it sends a standard HTTP 302 redirect.
131136
func handleUnauthorized(ctx *router.PageContext, loginPath string) {
132137
currentPath := ctx.Request.URL.Path
138+
133139
redirectURL := loginPath
134140
if currentPath != "" && currentPath != loginPath {
135141
redirectURL += "?redirect=" + url.QueryEscape(currentPath)
136142
}
137143

138-
isHTMX := ctx.Request.Header.Get("HX-Request") != ""
144+
isHTMX := ctx.Request.Header.Get("Hx-Request") != ""
139145

140146
if isHTMX {
141147
// HTMX partial request: use HX-Redirect header for client-side redirect.
142-
ctx.ResponseWriter.Header().Set("HX-Redirect", redirectURL)
148+
ctx.ResponseWriter.Header().Set("Hx-Redirect", redirectURL)
143149
ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
150+
144151
return
145152
}
146153

extensions/dashboard/auth/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func (u *UserInfo) HasRole(role string) bool {
9999
if u == nil {
100100
return false
101101
}
102+
102103
return slices.Contains(u.Roles, role)
103104
}
104105

@@ -107,6 +108,7 @@ func (u *UserInfo) HasScope(scope string) bool {
107108
if u == nil {
108109
return false
109110
}
111+
110112
return slices.Contains(u.Scopes, scope)
111113
}
112114

@@ -115,11 +117,13 @@ func (u *UserInfo) HasAnyRole(roles ...string) bool {
115117
if u == nil {
116118
return false
117119
}
120+
118121
for _, role := range roles {
119122
if slices.Contains(u.Roles, role) {
120123
return true
121124
}
122125
}
126+
123127
return false
124128
}
125129

@@ -128,7 +132,9 @@ func (u *UserInfo) GetClaim(key string) (any, bool) {
128132
if u == nil || u.Claims == nil {
129133
return nil, false
130134
}
135+
131136
val, ok := u.Claims[key]
137+
132138
return val, ok
133139
}
134140

@@ -138,13 +144,15 @@ func (u *UserInfo) Initials() string {
138144
if u != nil && u.Email != "" {
139145
return strings.ToUpper(u.Email[:1])
140146
}
147+
141148
return "?"
142149
}
143150

144151
parts := strings.Fields(u.DisplayName)
145152
if len(parts) == 1 {
146153
return strings.ToUpper(parts[0][:1])
147154
}
155+
148156
return strings.ToUpper(string(parts[0][0]) + string(parts[len(parts)-1][0]))
149157
}
150158

extensions/dashboard/aware.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dashboard
2+
3+
import (
4+
"github.com/xraph/forge/extensions/dashboard/contributor"
5+
"github.com/xraph/forgeui/bridge"
6+
)
7+
8+
// DashboardAware is an optional interface that Forge extensions can implement
9+
// to signal that they bundle a dashboard contributor. The dashboard extension
10+
// auto-discovers extensions implementing this interface during Start() and
11+
// registers their contributors automatically — no manual registration needed.
12+
//
13+
// The returned contributor can be an EmbeddedContributor (static build),
14+
// SSRContributor (Node.js sidecar), or any other LocalContributor.
15+
//
16+
// Example implementation in an extension:
17+
//
18+
// func (a *AuthExtension) DashboardContributor() contributor.LocalContributor {
19+
// return newDashboardContributor() // generated by forge contributor build
20+
// }
21+
type DashboardAware interface {
22+
DashboardContributor() contributor.LocalContributor
23+
}
24+
25+
// BridgeAware is an optional interface that Forge extensions can implement
26+
// to register custom Go↔JS bridge functions with the dashboard. Extensions
27+
// implementing this interface have RegisterDashboardBridge called during
28+
// dashboard Start() after the bridge is initialized.
29+
//
30+
// Example implementation:
31+
//
32+
// func (a *AuthExtension) RegisterDashboardBridge(b *bridge.Bridge) error {
33+
// return b.Register("auth.getStats", a.handleGetStats,
34+
// bridge.WithDescription("Get auth statistics"),
35+
// )
36+
// }
37+
type BridgeAware interface {
38+
RegisterDashboardBridge(b *bridge.Bridge) error
39+
}

extensions/dashboard/bridge.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package dashboard
22

33
import (
44
"context"
5-
"fmt"
5+
"errors"
66
"time"
77

88
"github.com/xraph/forgeui/bridge"
@@ -143,7 +143,7 @@ func (db *DashboardBridge) getServices(ctx bridge.Context, params EmptyParams) (
143143

144144
func (db *DashboardBridge) getServiceDetail(ctx bridge.Context, params ServiceNameParams) (*collector.ServiceDetail, error) {
145145
if params.Name == "" {
146-
return nil, fmt.Errorf("service name is required")
146+
return nil, errors.New("service name is required")
147147
}
148148

149149
return db.collector.CollectServiceDetail(context.Background(), params.Name), nil

extensions/dashboard/config.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dashboard
22

33
import (
4+
"errors"
45
"fmt"
56
"time"
67
)
@@ -25,7 +26,7 @@ type Config struct {
2526
MaxDataPoints int `json:"max_data_points" yaml:"max_data_points"`
2627

2728
// Proxy/Remote
28-
ProxyTimeout time.Duration `json:"proxy_timeout" yaml:"proxy_timeout"`
29+
ProxyTimeout time.Duration `json:"proxy_timeout" yaml:"proxy_timeout"`
2930
CacheMaxSize int `json:"cache_max_size" yaml:"cache_max_size"`
3031
CacheTTL time.Duration `json:"cache_ttl" yaml:"cache_ttl"`
3132

@@ -37,10 +38,10 @@ type Config struct {
3738
EnableCSRF bool `json:"enable_csrf" yaml:"enable_csrf"`
3839

3940
// Authentication
40-
EnableAuth bool `json:"enable_auth" yaml:"enable_auth"` // enable auth support
41-
LoginPath string `json:"login_path" yaml:"login_path"` // relative auth login path (e.g. "/auth/login")
42-
LogoutPath string `json:"logout_path" yaml:"logout_path"` // relative auth logout path (e.g. "/auth/logout")
43-
DefaultAccess string `json:"default_access" yaml:"default_access"` // "public", "protected", "partial"
41+
EnableAuth bool `json:"enable_auth" yaml:"enable_auth"` // enable auth support
42+
LoginPath string `json:"login_path" yaml:"login_path"` // relative auth login path (e.g. "/auth/login")
43+
LogoutPath string `json:"logout_path" yaml:"logout_path"` // relative auth logout path (e.g. "/auth/logout")
44+
DefaultAccess string `json:"default_access" yaml:"default_access"` // "public", "protected", "partial"
4445

4546
// Theming
4647
Theme string `json:"theme" yaml:"theme"` // light, dark, auto
@@ -102,7 +103,7 @@ func DefaultConfig() Config {
102103
// Validate validates the configuration.
103104
func (c Config) Validate() error {
104105
if c.BasePath == "" {
105-
return fmt.Errorf("dashboard: base_path cannot be empty")
106+
return errors.New("dashboard: base_path cannot be empty")
106107
}
107108

108109
if c.RefreshInterval < time.Second {

0 commit comments

Comments
 (0)