Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
30f166c
docs(dashboard/contract): add slice-a design spec and implementation …
juicycleff May 9, 2026
cda94a1
feat(dashboard/contract): add package skeleton and canonical error codes
juicycleff May 9, 2026
d68c5ab
test(dashboard/contract): make canonical-code presence test meaningful
juicycleff May 9, 2026
16c3aae
feat(dashboard/contract): add wire envelope types for request, respon…
juicycleff May 9, 2026
97d4de1
test(dashboard/contract): cover error response and stream event round…
juicycleff May 9, 2026
78ab341
feat(dashboard/contract): add manifest types with YAML tags
juicycleff May 9, 2026
64de364
feat(dashboard/contract): parse data binding shorthand and inline forms
juicycleff May 9, 2026
b71788d
feat(dashboard/contract): support param source shorthand in YAML
juicycleff May 9, 2026
8f0f1fc
feat(dashboard/contract): boolean predicate evaluator and stable perm…
juicycleff May 9, 2026
17d9497
feat(dashboard/contract): warden interface and in-memory registry
juicycleff May 9, 2026
0568c10
fix(dashboard/contract): Action.Kind uses wire Kind not manifest Inte…
juicycleff May 9, 2026
aa6c34d
feat(dashboard/contract): YAML loader with schemaVersion check
juicycleff May 9, 2026
a402c55
feat(dashboard/contract): cross-reference validator (intents, queries…
juicycleff May 9, 2026
5c626a3
feat(dashboard/contract): contract registry with slot catalog and cro…
juicycleff May 9, 2026
cf19ed3
feat(dashboard/contract): per-request graph filter by visibleWhen + w…
juicycleff May 9, 2026
ae5e4c4
feat(dashboard/contract): LRU+TTL graph cache keyed by (route, permis…
juicycleff May 9, 2026
6733263
feat(dashboard/contract): audit emitter interface with log-based default
juicycleff May 9, 2026
67c02fb
test(dashboard/contract): add manifest test helper for sibling packages
juicycleff May 9, 2026
d9538f3
feat(dashboard/contract): POST handler with kind dispatch, version + …
juicycleff May 9, 2026
b8b9c50
feat(dashboard/contract): capabilities endpoint for version negotiation
juicycleff May 9, 2026
aefecc8
feat(dashboard/contract): multiplexed SSE stream broker with subscrip…
juicycleff May 9, 2026
9899a98
feat(dashboard): allow legacy Manifest to carry a contract manifest
juicycleff May 9, 2026
1e05dc2
feat(dashboard): wire contract endpoints and contributor registration
juicycleff May 9, 2026
4b05bb8
feat(cmd): add dashboard-contract-probe CLI for raw envelope testing
juicycleff May 9, 2026
c5f5c36
test(dashboard/contract): end-to-end fixture + driver covering regist…
juicycleff May 9, 2026
7e91bda
docs(dashboard/contract): add slice-c dispatcher and pilot migration …
juicycleff May 10, 2026
eb3e42b
docs(dashboard/contract): add slice-c implementation plan
juicycleff May 10, 2026
3176726
feat(dashboard/contract/dispatcher): add package skeleton and handler…
juicycleff May 10, 2026
1dcb38b
feat(dashboard/contract/dispatcher): function-table dispatcher with c…
juicycleff May 10, 2026
2afe407
feat(dashboard/contract/dispatcher): subscription handler registratio…
juicycleff May 10, 2026
912ec62
feat(dashboard/contract/dispatcher): generic typed wrappers for query…
juicycleff May 10, 2026
c5b98a4
test(dashboard/contract/dispatcher): cover metrics emission for succe…
juicycleff May 10, 2026
796396a
feat(dashboard/contract/dispatcher): contributor interface registrati…
juicycleff May 10, 2026
89ac07f
feat(dashboard/contract/pilot): payload types for the pilot intents
juicycleff May 10, 2026
e2c1ddf
feat(dashboard/contract/pilot): embedded YAML manifest with three rou…
juicycleff May 10, 2026
80a2ec8
feat(dashboard/contract/pilot): query handlers for extensions, servic…
juicycleff May 10, 2026
4908db4
feat(dashboard/contract/pilot): metrics.summary replace-mode subscrip…
juicycleff May 10, 2026
dc16545
feat(dashboard/contract/pilot): Register entry point wires manifest +…
juicycleff May 10, 2026
c10acbc
feat(dashboard): wire real dispatcher, stream broker, and pilot contr…
juicycleff May 10, 2026
f870773
test(dashboard/contract/pilot): end-to-end HTTP + SSE round-trip thro…
juicycleff May 10, 2026
380d79e
docs(dashboard/contract): add slice-b security and observability design
juicycleff May 10, 2026
53f7ff9
docs(dashboard/contract): add slice-b implementation plan
juicycleff May 10, 2026
88883a8
feat(dashboard/contract/idempotency): Store interface and Cached entry
juicycleff May 10, 2026
2248f88
feat(dashboard/contract/idempotency): in-memory Store with TTL and LR…
juicycleff May 10, 2026
c5fa185
feat(dashboard/contract): CSRF token issuance endpoint
juicycleff May 10, 2026
dbbbb56
feat(dashboard/contract): CSRF token validation for command envelopes
juicycleff May 10, 2026
8a75376
feat(dashboard/contract/dispatcher): Prometheus-backed MetricsEmitter
juicycleff May 10, 2026
5a7074a
feat(dashboard/contract/dispatcher): structured-logger AuditEmitter
juicycleff May 10, 2026
cf62569
feat(dashboard/contract/dispatcher): optional OTel tracer with span l…
juicycleff May 10, 2026
ede482f
feat(dashboard/contract/dispatcher): idempotency-key dedup for comman…
juicycleff May 10, 2026
635132b
feat(dashboard): wire CSRF, idempotency, Prometheus metrics, OTel tra…
juicycleff May 10, 2026
e15f6c0
test(dashboard/contract): integration test for CSRF + idempotency end…
juicycleff May 10, 2026
fc38297
docs(dashboard/contract): add slice-d React shell rendering engine de…
juicycleff May 10, 2026
2c30118
docs(dashboard/contract): add slice-d implementation plan
juicycleff May 10, 2026
5b9ec20
feat(dashboard/contract/shell): scaffold React+TypeScript+Vite project
juicycleff May 10, 2026
83eee34
feat(dashboard/contract/shell): minimal React app skeleton
juicycleff May 10, 2026
792e4b8
feat(dashboard/contract/shell): contract client with auto-CSRF and id…
juicycleff May 10, 2026
ecc7614
feat(dashboard/contract/shell): SSE multiplex consumer
juicycleff May 10, 2026
5958746
feat(dashboard/contract/shell): principal store + React Query hooks f…
juicycleff May 10, 2026
c321dd5
feat(dashboard/contract/shell): intent registry, graph renderer, slot…
juicycleff May 10, 2026
6c25b43
feat(dashboard/contract/shell): page.shell and metric.counter intent …
juicycleff May 10, 2026
a80cccb
feat(dashboard/contract/shell): App routing + end-to-end smoke test
juicycleff May 10, 2026
1bce87d
feat(dashboard): principal endpoint, shell embed, and SPA route regis…
juicycleff May 10, 2026
1410c05
docs(dashboard/contract): add slice-e built-in vocabulary design (sha…
juicycleff May 10, 2026
1b365e8
feat(dashboard/contract/shell): shadcn infrastructure (Radix deps, th…
juicycleff May 10, 2026
03e982d
feat(dashboard/contract/shell): refactor existing components onto sha…
juicycleff May 10, 2026
32679ec
feat(dashboard/contract/shell): action vocabulary (button, menu, divi…
juicycleff May 10, 2026
8b70148
feat(dashboard/contract/shell): form vocabulary (form.edit, form.field)
juicycleff May 10, 2026
5772bac
feat(dashboard/contract/shell): resource vocabulary (resource.list, r…
juicycleff May 10, 2026
d9b707d
feat(dashboard/contract/shell): audit.tail live append-mode subscription
juicycleff May 10, 2026
1a3268a
docs(dashboard/contract/shell): expanded README + ARCHITECTURE.md
juicycleff May 10, 2026
35ef4c9
feat(dashboard/contract/shell): swap shadcn primitives from Radix to …
juicycleff May 10, 2026
bea27c6
docs(dashboard/contract): add slice-f streaming migration design
juicycleff May 10, 2026
89b4d7b
feat(streaming): implement contract-based dashboard integration
juicycleff May 10, 2026
ec0eff3
test(streaming/contract): unit tests for slice (f) handlers + manifest
juicycleff May 10, 2026
a167a70
docs(dashboard/contract): add slice-h CoreContributor migration design
juicycleff May 10, 2026
b926658
feat(dashboard/contract): slice (h) extend core-contract pilot to all…
juicycleff May 10, 2026
08d4271
feat(dashboard): slice (i) retire CoreContributor templ pages, redire…
juicycleff May 10, 2026
0f02eec
feat(dashboard/contract): slice (j) wire graph endpoint, add /traces/…
juicycleff May 10, 2026
2f9709d
feat(dashboard/contract): slice (k) audit storage + live audit.tail s…
juicycleff May 10, 2026
7cc467e
fix(dashboard/contract): derive shell endpoints from runtime config s…
juicycleff May 10, 2026
1cb135d
feat(dashboard/contract): slice (l) shadcn dashboard layout + optiona…
juicycleff May 10, 2026
eaad50b
feat(dashboard/contract): AuthGate prefers extension-contributed /log…
juicycleff May 10, 2026
8fd2ae6
feat(dashboard/contract): expose ResponseWriter + Request to command …
juicycleff May 10, 2026
867298c
feat(dashboard/contract): shadcn login-04 visuals + dynamic providers…
juicycleff May 10, 2026
5070b3d
feat(dashboard/contract): remote contract contributors — multi-servic…
juicycleff May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ METRICS_DEADLOCK_FIX.md
!README.md
**/*.md
!**/README.md
!extensions/dashboard/contract/*.md
!extensions/dashboard/contract/shell/*.md
!extensions/dashboard/contract/shell/test/
!extensions/dashboard/contract/shell/test/**
/**/*.disabled
extensions/dashboard/forgeui
*.blob
Expand Down
41 changes: 41 additions & 0 deletions cmd/dashboard-contract-probe/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// main.go
package main

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"

"github.com/xraph/forge/extensions/dashboard/contract"
)

func main() {
base := flag.String("base", "http://localhost:8080", "dashboard base URL (no trailing slash)")
kind := flag.String("kind", "query", "graph | query | command")
contributor := flag.String("contributor", "", "contributor name")
intent := flag.String("intent", "", "intent name")
payload := flag.String("payload", "{}", "JSON payload")
csrf := flag.String("csrf", "", "CSRF token (required for command)")
idem := flag.String("idem", "", "idempotency key (required for command)")
flag.Parse()

req := contract.Request{
Envelope: "v1", Kind: contract.Kind(*kind),
Contributor: *contributor, Intent: *intent,
Payload: json.RawMessage(*payload),
CSRF: *csrf, IdempotencyKey: *idem,
}
body, _ := json.Marshal(req)
resp, err := http.Post(*base+"/api/dashboard/v1", "application/json", bytes.NewReader(body))
if err != nil {
fmt.Fprintln(os.Stderr, "request:", err)
os.Exit(1)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Printf("HTTP %d\n%s\n", resp.StatusCode, out)
}
62 changes: 55 additions & 7 deletions cmd/forge/plugins/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -641,6 +642,35 @@ func newAppWatcher(cfg *config.ForgeConfig, app *AppInfo) (*appWatcher, error) {
// have been extracted to dev_shared.go as package-level functions shared with
// dockerAppWatcher.

// buildApp compiles the user's main package into a stable per-app path inside
// the project's .forge/dev directory. We rebuild in place each time so the
// path stays predictable (helpful for debuggers and codesigning) and old
// artifacts don't accumulate in the OS temp dir.
func (aw *appWatcher) buildApp() (string, error) {
buildDir := filepath.Join(aw.config.RootDir, ".forge", "dev")
if err := os.MkdirAll(buildDir, 0o755); err != nil {
return "", fmt.Errorf("create build dir: %w", err)
}

binName := aw.app.Name
if binName == "" {
binName = "app"
}
if runtime.GOOS == "windows" {
binName += ".exe"
}
binPath := filepath.Join(buildDir, binName)

cmd := exec.Command("go", "build", "-tags", "forge_debug", "-o", binPath, aw.mainFile)
cmd.Dir = aw.config.RootDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("go build: %w", err)
}
return binPath, nil
}

// Start starts the application process.
func (aw *appWatcher) Start(ctx cli.CommandContext) error {
aw.mu.Lock()
Expand Down Expand Up @@ -671,9 +701,19 @@ func (aw *appWatcher) Start(ctx cli.CommandContext) error {

ctx.Success(fmt.Sprintf("Starting %s...", aw.app.Name))

// Create new command — compile with forge_debug tag so the in-process
// debug server is included; disabled automatically in release builds.
cmd := exec.Command("go", "run", "-tags", "forge_debug", aw.mainFile)
// Build the binary first, then exec it directly. We deliberately avoid
// `go run` here: it spawns the compiled binary as a grandchild, and on
// abrupt shutdown (kill -9 of forge dev, parent shell exit) those
// grandchildren get reparented to PID 1 and survive with their dispatch
// loops still hammering the database. Building once and exec'ing the
// binary directly means there's exactly one process to track and
// killProcessGroup reliably reaches it.
binPath, err := aw.buildApp()
if err != nil {
return fmt.Errorf("build failed: %w", err)
}

cmd := exec.Command(binPath)
cmd.Dir = aw.config.RootDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
Expand Down Expand Up @@ -1000,11 +1040,15 @@ func (p *DevPlugin) startContributorDevServers(ctx cli.CommandContext) []*contri
devCmd = adapter.DefaultDevCmd()
}

// Start the dev server
// Start the dev server. Put the shell in its own process group so
// killProcessGroup can take down the whole tree (npm/node/vite/...)
// instead of just the sh wrapper, which would otherwise leave the
// real dev server orphaned on rebuild.
cmd := exec.Command("sh", "-c", devCmd)
cmd.Dir = uiDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
setupProcessGroup(cmd)

if err := cmd.Start(); err != nil {
ctx.Warning(fmt.Sprintf("Failed to start contributor dev server %s: %v", cfg.Name, err))
Expand All @@ -1024,9 +1068,13 @@ func (p *DevPlugin) startContributorDevServers(ctx cli.CommandContext) []*contri
// stopContributorDevServers stops all running contributor dev server processes.
func stopContributorDevServers(processes []*contributorDevProcess) {
for _, proc := range processes {
if proc.Cmd != nil && proc.Cmd.Process != nil {
proc.Cmd.Process.Kill()
proc.Cmd.Process.Wait()
if proc.Cmd == nil || proc.Cmd.Process == nil {
continue
}
// killProcessGroup tears down the whole sh→npm→node tree. A bare
// Process.Kill would only stop the sh wrapper, leaving npm/node
// running and holding the dev port.
killProcessGroup(proc.Cmd)
_, _ = proc.Cmd.Process.Wait()
}
}
39 changes: 38 additions & 1 deletion extensions/dashboard/auth/context.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dashauth

import "context"
import (
"context"
"net/http"
)

type contextKey string

Expand Down Expand Up @@ -47,3 +50,37 @@ func HasTenantInContext(ctx context.Context) bool {

return t.HasTenant()
}

const (
respWriterContextKey contextKey = "forge:dashboard:resp"
requestContextKey contextKey = "forge:dashboard:req"
)

// WithHTTP stashes the live ResponseWriter and Request on ctx so contract
// command handlers that legitimately need to touch HTTP — e.g. the auth
// extension's auth.login handler that issues a Set-Cookie — can reach them.
// The contract transport calls this before dispatching commands.
//
// Most contract handlers are pure data and should NOT pull these out;
// reaching for the response writer is an escape hatch for the small set of
// concerns where the cookie/header IS the contract (auth, downloads).
func WithHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context {
ctx = context.WithValue(ctx, respWriterContextKey, w)
ctx = context.WithValue(ctx, requestContextKey, r)
return ctx
}

// ResponseWriterFromContext returns the ResponseWriter previously stashed by
// WithHTTP, or nil when called outside an HTTP-served dispatch (background
// jobs, tests).
func ResponseWriterFromContext(ctx context.Context) http.ResponseWriter {
w, _ := ctx.Value(respWriterContextKey).(http.ResponseWriter)
return w
}

// RequestFromContext returns the Request previously stashed by WithHTTP, or
// nil outside an HTTP-served dispatch.
func RequestFromContext(ctx context.Context) *http.Request {
r, _ := ctx.Value(requestContextKey).(*http.Request)
return r
}
71 changes: 71 additions & 0 deletions extensions/dashboard/aware.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dashboard

import (
"github.com/xraph/forge/extensions/dashboard/contract"
"github.com/xraph/forge/extensions/dashboard/contract/dispatcher"
"github.com/xraph/forge/extensions/dashboard/contributor"
"github.com/xraph/forge/extensions/dashboard/ui/shell"
"github.com/xraph/forgeui/bridge"
Expand Down Expand Up @@ -52,6 +54,43 @@ type BridgeAware interface {
// ext.SetAuthChecker(myAuthChecker)
// ext.EnableAuth()
// }
//
// Slice (l) note — wiring an auth extension into the contract React shell:
//
// The dashboard shell ships a built-in LoginScreen that submits a contract
// command (default `auth.login`) and reloads the principal on success. Auth
// extensions can plug in by implementing both DashboardAuthAware *and*
// ContractContributorAware:
//
// - DashboardAuthAware.RegisterDashboardAuth wires the AuthChecker so
// /api/dashboard/v1/principal returns the current user. The shell's
// AuthGate listens for the 401 envelope (auth required) vs the 200
// `{authenticated:false}` envelope (auth disabled) and renders the
// LoginScreen only in the former case.
// - ContractContributorAware.RegisterContractContributor registers the
// `auth.login` command intent (and optionally `auth.logout`) on the
// dispatcher. The built-in LoginScreen issues the command; an
// extension that wants a richer flow can also publish a `/login`
// graph route in its manifest, and the shell's AuthGate will render
// that page instead of the built-in form.
//
// Example combined integration sketch:
//
// func (a *AuthsomeExtension) RegisterDashboardAuth(ext *dashboard.Extension) {
// ext.SetAuthChecker(a.checker)
// ext.EnableAuth()
// }
// func (a *AuthsomeExtension) RegisterContractContributor(
// disp *dispatcher.Dispatcher,
// reg contract.Registry,
// wreg contract.WardenRegistry,
// ) error {
// return authsomecontract.Register(disp, reg, wreg, authsomecontract.Deps{
// Sessions: a.sessions,
// // Registers `auth.login` (command) and optionally a `/login`
// // graph route that overrides the built-in LoginScreen.
// })
// }
type DashboardAuthAware interface {
RegisterDashboardAuth(ext *Extension)
}
Expand All @@ -72,3 +111,35 @@ type DashboardAuthAware interface {
type DashboardFooterContributor interface {
DashboardUserDropdownActions(basePath string) []shell.UserDropdownAction
}

// ContractContributorAware is an optional interface that Forge extensions can
// implement to register a contract-based dashboard contributor (the slice (f)+
// shape — declarative YAML manifest + typed dispatcher handlers). The
// dashboard auto-discovers extensions implementing this interface during
// Start() and calls RegisterContractContributor with the dashboard's contract
// registry, warden registry, and dispatcher.
//
// Coexists with DashboardAware: an extension can implement both to register
// its legacy templ-based contributor AND its contract contributor during the
// migration window. New extensions should prefer this interface; legacy ones
// migrate over time per the per-slice (f, g, h) plan.
//
// Example implementation:
//
// func (e *StreamingExtension) RegisterContractContributor(
// disp *dispatcher.Dispatcher,
// reg contract.Registry,
// wreg contract.WardenRegistry,
// ) error {
// return streamingcontract.Register(disp, reg, wreg, streamingcontract.Deps{
// Manager: func() streaming.Manager { return e.manager },
// Config: func() streaming.Config { return e.config },
// })
// }
type ContractContributorAware interface {
RegisterContractContributor(
disp *dispatcher.Dispatcher,
reg contract.Registry,
wreg contract.WardenRegistry,
) error
}
32 changes: 30 additions & 2 deletions extensions/dashboard/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,22 @@ type Config struct {
// Security
EnableCSP bool `json:"enable_csp" yaml:"enable_csp"`
EnableCSRF bool `json:"enable_csrf" yaml:"enable_csrf"`
// EnableContractSecurity gates CSRF validation, idempotency dedup, and
// distributed tracing on the contract envelope endpoint. Default true;
// set to false during a rollout window where clients have not yet
// adopted CSRF tokens or the idempotency-key contract.
EnableContractSecurity bool `json:"enable_contract_security" yaml:"enable_contract_security"`

// Authentication
EnableAuth bool `json:"enable_auth" yaml:"enable_auth"` // enable auth support
LoginPath string `json:"login_path" yaml:"login_path"` // relative auth login path (e.g. "/auth/login")
LogoutPath string `json:"logout_path" yaml:"logout_path"` // relative auth logout path (e.g. "/auth/logout")
DefaultAccess string `json:"default_access" yaml:"default_access"` // "public", "protected", "partial"
// RequiredRoles, when non-empty, restricts dashboard access to users
// carrying at least one matching role. The principal endpoint surfaces
// 403 PERMISSION_DENIED to the React shell for users who don't qualify;
// the shell renders an "access denied" panel instead of the dashboard.
RequiredRoles []string `json:"required_roles" yaml:"required_roles"`

// Theming
Theme string `json:"theme" yaml:"theme"` // light, dark, auto
Expand Down Expand Up @@ -101,8 +111,9 @@ func DefaultConfig() Config {

SSEKeepAlive: 15 * time.Second,

EnableCSP: true,
EnableCSRF: true,
EnableCSP: true,
EnableCSRF: true,
EnableContractSecurity: true,

EnableAuth: false,
LoginPath: "/login",
Expand Down Expand Up @@ -255,6 +266,14 @@ func WithCSRF(enabled bool) ConfigOption {
return func(c *Config) { c.EnableCSRF = enabled }
}

// WithContractSecurity enables or disables the contract envelope's
// security stack (CSRF validation, idempotency dedup, request tracing).
// Defaults to true; switching off should be reserved for rollout windows
// where clients have not yet adopted CSRF tokens or idempotency keys.
func WithContractSecurity(enabled bool) ConfigOption {
return func(c *Config) { c.EnableContractSecurity = enabled }
}

// WithTheme sets the UI theme (light, dark, auto).
func WithTheme(theme string) ConfigOption {
return func(c *Config) { c.Theme = theme }
Expand Down Expand Up @@ -285,6 +304,15 @@ func WithEnableAuth(enabled bool) ConfigOption {
return func(c *Config) { c.EnableAuth = enabled }
}

// WithRequiredRoles restricts dashboard access to users carrying at least
// one of the given roles. Pass nil/empty to allow all authenticated users.
// Auth extensions (e.g. authsome) call this via Extension.SetRequiredRoles
// when their config declares a role gate; deployments can also configure
// it directly via this option.
func WithRequiredRoles(roles []string) ConfigOption {
return func(c *Config) { c.RequiredRoles = append([]string(nil), roles...) }
}

// WithLoginPath sets the relative login page path (e.g. "/auth/login").
func WithLoginPath(path string) ConfigOption {
return func(c *Config) { c.LoginPath = path }
Expand Down
Loading
Loading