Skip to content

Commit 9d234d5

Browse files
authored
docs: source homepage and Casbin examples from runnable files (#461)
* docs: source homepage and Casbin examples from runnable files * docs: define Casbin enforcer on page and keep hero code out of tab order - Include the enforcer setup from cookbook/casbin/server.go so the JWT and Basic Auth excerpts no longer reference an undefined `ce`; exit when the enforcer fails to load instead of continuing with a nil enforcer. - List echo-jwt and golang-jwt in Dependencies for all locales. - Strip Shiki's tabindex from the hero <pre>: the code window is aria-hidden, so a focusable child fails axe aria-hidden-focus.
1 parent 0e7e4df commit 9d234d5

10 files changed

Lines changed: 116 additions & 563 deletions

File tree

‎cookbook/casbin/server.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package main
33
import (
44
"log/slog"
55
"net/http"
6+
"os"
67

78
"github.com/casbin/casbin/v3"
89
"github.com/golang-jwt/jwt/v5"
910
echojwt "github.com/labstack/echo-jwt/v5"
1011
"github.com/labstack/echo/v5"
1112
)
1213

14+
// docs:start middleware
1315
// NewCasbinMiddleware returns middleware for [Casbin](https://casbin.org/).
1416
func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc {
1517
return func(next echo.HandlerFunc) echo.HandlerFunc {
@@ -28,17 +30,22 @@ func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Contex
2830
}
2931
}
3032

33+
// docs:end middleware
34+
3135
/*
3236
Test with:
3337
curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
3438
*/
3539
func main() {
3640
e := echo.New()
3741

42+
// docs:start enforcer
3843
ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
3944
if err != nil {
4045
slog.Error("failed to initialize Casbin enforcer", "error", err)
46+
os.Exit(1)
4147
}
48+
// docs:end enforcer
4249

4350
// BasicAuth middleware does authentication
4451
// - should pass `curl -v -u "alice:password" http://localhost:8080/dataset1/any`
@@ -53,6 +60,7 @@ func main() {
5360
//}
5461
//e.Use(NewCasbinMiddleware(ce, basicAuthUser)) // Casbin does authorization
5562

63+
// docs:start jwt
5664
e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication
5765
jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization
5866
token, err := echo.ContextGet[*jwt.Token](c, "user")
@@ -62,6 +70,7 @@ func main() {
6270
return token.Claims.GetSubject()
6371
}
6472
e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization
73+
// docs:end jwt
6574

6675
e.GET("/*", func(c *echo.Context) error {
6776
return c.String(http.StatusOK, "Hello, World!")

‎cookbook/hello-world/server.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
func main() {
1212
// Echo instance
13+
// docs:start hero
1314
e := echo.New()
1415

1516
// Middleware
@@ -20,6 +21,7 @@ func main() {
2021
e.GET("/", func(c *echo.Context) error {
2122
return c.String(http.StatusOK, "Hello, World!\n")
2223
})
24+
// docs:end hero
2325

2426
// Start server
2527
sc := echo.StartConfig{Address: ":1323"}

‎site/plugins/remark-source-code.mjs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ function readSnippet(specifier, root) {
6666
} catch (error) {
6767
throw new Error(`Cannot include ${path}: ${error.message}`);
6868
}
69-
const lines = source.split('\n');
69+
return sourceSnippet(source, region, path);
70+
}
71+
72+
/** Extract the same source regions from Vite raw imports and Markdown includes. */
73+
export function sourceSnippet(source, region, path = 'source') {
74+
const lines = source.replace(/\r\n/g, '\n').split('\n');
7075
if (region !== undefined) {
7176
const starts = [];
7277
const ends = [];

‎site/plugins/remark-source-code.test.mjs‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { join } from 'node:path';
55
import test from 'node:test';
66
import { fileURLToPath } from 'node:url';
77
import { unified } from 'unified';
8-
import remarkSourceCode, { sourceReference, withSourceCode } from './remark-source-code.mjs';
8+
import remarkSourceCode, { sourceReference, sourceSnippet, withSourceCode } from './remark-source-code.mjs';
99
import { parseSourceDocument, visitCode } from './source-document.mjs';
1010
import { localePrefixes } from '../src/locales.mjs';
1111

@@ -212,3 +212,24 @@ test('every cookbook page in every locale displays source-owned programs and exc
212212
}
213213
}
214214
});
215+
216+
test('raw component imports and Markdown includes share source excerpts; Casbin middleware appears once per locale', () => {
217+
const root = fileURLToPath(new URL('../..', import.meta.url));
218+
const path = 'cookbook/hello-world/server.go';
219+
const code = sourceSnippet(readFileSync(join(root, path), 'utf8'), 'hero', path);
220+
const [included] = programFences(render(`\x60\x60\x60go file=${path}#hero\n\x60\x60\x60`, root));
221+
assert.equal(code, included.value);
222+
assert.match(code, /e\.GET\("\/"/);
223+
assert.doesNotMatch(code, /docs:(start|end)/);
224+
for (const locale of localePrefixes) {
225+
const page = join(root, 'site/src/content/docs', locale, 'middleware/casbin-auth.md');
226+
const rendered = render(readFileSync(page, 'utf8'), root, page);
227+
const fences = programFences(rendered);
228+
const declarations = fences.flatMap((fence) => [...fence.value.matchAll(/^func NewCasbinMiddleware\b/gm)]);
229+
assert.equal(declarations.length, 1, `${locale}Casbin middleware has a single source-backed definition`);
230+
assert.ok(fences.some((fence) => fence.value.includes('enforcer.Enforce(')));
231+
assert.ok(fences.some((fence) => fence.value.includes('casbin.NewEnforcer(')), `${locale}the enforcer used by the excerpts is defined`);
232+
assert.ok(fences.some((fence) => fence.value.includes('echojwt.JWT(')));
233+
assert.ok(!fences.some((fence) => /^package main\b/m.test(fence.value)), 'the full server is linked rather than duplicated');
234+
}
235+
});

‎site/src/components/HomeHero.astro‎

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
---
22
// Living Terminal hero: code window + a terminal pane that types `curl`
3-
// and streams Echo's JSON response. Animation is client-side, with a
4-
// static final-state fallback for reduced-motion / no-JS.
3+
// and shows the runnable example's response. Animation is client-side, with a
4+
// static final-state fallback for reduced motion.
55
import { starsLabel } from '../data/github.ts';
66
import stable from '../generated/echo-source.json';
7+
import { Code } from 'astro:components';
8+
import helloWorldSource from '../../../cookbook/hello-world/server.go?raw';
9+
import { sourceSnippet } from '../../plugins/remark-source-code.mjs';
710
const next = process.env.DOCS_CHANNEL === 'next';
11+
const helloWorldCode = sourceSnippet(helloWorldSource, 'hero', 'cookbook/hello-world/server.go');
12+
// The code window is aria-hidden, so Shiki's <pre> must not be a tab stop.
13+
const notFocusable = { pre(node: { properties: Record<string, unknown> }) { delete node.properties.tabindex; } };
814
---
915

1016
<section class="hh">
@@ -28,19 +34,9 @@ const next = process.env.DOCS_CHANNEL === 'next';
2834
<div class="hh-win" aria-hidden="true">
2935
<div class="bar">
3036
<i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i>
31-
<span class="fn">main.go</span>
37+
<span class="fn">server.go · excerpt</span>
3238
</div>
33-
<pre><span class="kw">package</span> main
34-
35-
<span class="kw">import</span> <span class="str">"github.com/labstack/echo/v5"</span>
36-
37-
<span class="kw">func</span> <span class="fnn">main</span>() &#123;
38-
e := echo.<span class="fnn">New</span>()
39-
e.<span class="fnn">GET</span>(<span class="str">"/"</span>, <span class="kw">func</span>(c *echo.Context) <span class="kw">error</span> &#123;
40-
<span class="kw">return</span> c.<span class="fnn">JSON</span>(<span class="str">200</span>, <span class="kw">map</span>[<span class="kw">string</span>]<span class="kw">string</span>&#123;<span class="str">"message"</span>: <span class="str">"Hello, World!"</span>&#125;)
41-
&#125;)
42-
e.<span class="fnn">Start</span>(<span class="str">":1323"</span>)
43-
&#125;</pre>
39+
<Code code={helloWorldCode} lang="go" style="background: transparent" transformers={[notFocusable]} />
4440
<div class="hh-term" data-term>
4541
<div><span class="pr">~/echo $</span> <span data-cmd></span><span class="hh-cur" data-cur></span></div>
4642
<div data-out></div>
@@ -77,8 +73,8 @@ const next = process.env.DOCS_CHANNEL === 'next';
7773
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
7874

7975
const response =
80-
'<span class="dim">HTTP/1.1 200 OK · 0 allocations</span>\n' +
81-
'<span class="json">{ "message": "Hello, World!" }</span>';
76+
'<span class="dim">HTTP/1.1 200 OK</span>\n' +
77+
'<span class="json">Hello, World!</span>';
8278
const cmd = 'curl localhost:1323';
8379

8480
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

‎site/src/content/docs/es/middleware/casbin-auth.md‎

Lines changed: 13 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@ Consulta el [resumen de API](https://casbin.org/docs/api-overview) y la
2626

2727
```bash
2828
go get github.com/casbin/casbin/v3
29+
go get github.com/labstack/echo-jwt/v5
30+
go get github.com/golang-jwt/jwt/v5
2931
```
3032

3133
```go
3234
import (
3335
"github.com/casbin/casbin/v3"
36+
"github.com/golang-jwt/jwt/v5"
37+
echojwt "github.com/labstack/echo-jwt/v5"
3438
)
3539
```
3640

@@ -39,58 +43,24 @@ import (
3943
Echo no incluye un middleware Casbin; la integración es un wrapper pequeño alrededor del
4044
enforcer de Casbin:
4145

42-
```go
43-
// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/).
44-
func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc {
45-
return func(next echo.HandlerFunc) echo.HandlerFunc {
46-
return func(c *echo.Context) error {
47-
username, err := userGetter(c)
48-
if err != nil {
49-
return echo.ErrUnauthorized.Wrap(err)
50-
}
51-
if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil {
52-
return echo.ErrInternalServerError.Wrap(err)
53-
} else if !pass {
54-
return echo.NewHTTPError(http.StatusForbidden, "access denied")
55-
}
56-
return next(c)
57-
}
58-
}
59-
}
46+
```go file=cookbook/casbin/server.go#middleware
6047
```
6148

6249
## Ejemplo
6350

6451
Crea un archivo de modelo Casbin `auth_model.conf`:
6552

66-
```ini
67-
[request_definition]
68-
r = sub, obj, act
69-
70-
[policy_definition]
71-
p = sub, obj, act
72-
73-
[role_definition]
74-
g = _, _
53+
```ini file=cookbook/casbin/auth_model.conf
54+
```
7555

76-
[policy_effect]
77-
e = some(where (p.eft == allow))
56+
Crea un archivo de policy Casbin `auth_policy.csv`:
7857

79-
[matchers]
80-
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
58+
```csv file=cookbook/casbin/auth_policy.csv
8159
```
8260

83-
Crea un archivo de policy Casbin `auth_policy.csv`:
61+
Carga el modelo y la policy en un enforcer de Casbin:
8462

85-
```csv
86-
p, 1234567890, /dataset1/*, GET
87-
p, alice, /dataset1/*, GET
88-
p, alice, /dataset1/resource1, POST
89-
p, bob, /dataset2/resource1, *
90-
p, bob, /dataset2/resource2, GET
91-
p, bob, /dataset2/folder1/*, POST
92-
p, dataset1_admin, /dataset1/*, *
93-
g, cathy, dataset1_admin
63+
```go file=cookbook/casbin/server.go#enforcer
9464
```
9565

9666
La autenticación y la autorización son responsabilidades separadas. Autentica al usuario con
@@ -99,16 +69,7 @@ pueda autorizar el request.
9969

10070
### Con JWT
10171

102-
```go
103-
e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication
104-
jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization
105-
token, err := echo.ContextGet[*jwt.Token](c, "user")
106-
if err != nil {
107-
return "", err
108-
}
109-
return token.Claims.GetSubject()
110-
}
111-
e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization
72+
```go file=cookbook/casbin/server.go#jwt
11273
```
11374

11475
Pruébalo con:
@@ -143,61 +104,4 @@ curl -v -u "alice:password" http://localhost:8080/dataset2/resource2
143104

144105
### Ejemplo completo de Casbin + JWT
145106

146-
```go
147-
package main
148-
149-
import (
150-
"log/slog"
151-
"net/http"
152-
153-
"github.com/casbin/casbin/v3"
154-
"github.com/golang-jwt/jwt/v5"
155-
echojwt "github.com/labstack/echo-jwt/v5"
156-
"github.com/labstack/echo/v5"
157-
)
158-
159-
// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/).
160-
func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc {
161-
return func(next echo.HandlerFunc) echo.HandlerFunc {
162-
return func(c *echo.Context) error {
163-
username, err := userGetter(c)
164-
if err != nil {
165-
return echo.ErrUnauthorized.Wrap(err)
166-
}
167-
if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil {
168-
return echo.ErrInternalServerError.Wrap(err)
169-
} else if !pass {
170-
return echo.NewHTTPError(http.StatusForbidden, "access denied")
171-
}
172-
return next(c)
173-
}
174-
}
175-
}
176-
177-
func main() {
178-
e := echo.New()
179-
180-
ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv")
181-
if err != nil {
182-
slog.Error("failed to initialize Casbin enforcer", "error", err)
183-
}
184-
185-
e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication
186-
jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization
187-
token, err := echo.ContextGet[*jwt.Token](c, "user")
188-
if err != nil {
189-
return "", err
190-
}
191-
return token.Claims.GetSubject()
192-
}
193-
e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization
194-
195-
e.GET("/*", func(c *echo.Context) error {
196-
return c.String(http.StatusOK, "Hello, World!")
197-
})
198-
199-
if err := e.Start(":8080"); err != nil {
200-
e.Logger.Error("failed to start server", "error", err)
201-
}
202-
}
203-
```
107+
Ejecuta el [ejemplo completo de Casbin + JWT](https://github.com/labstack/echox/tree/master/cookbook/casbin) con los archivos de modelo y política anteriores.

0 commit comments

Comments
 (0)