Skip to content

Commit 483a85e

Browse files
committed
Improve the "protecting your first app" guide
- Add a warning around use of zedtokens or consistency - Implement a handler for adjusting code samples based on entered endpoint, tenant, token etc
1 parent 470837d commit 483a85e

File tree

8 files changed

+2738
-2465
lines changed

8 files changed

+2738
-2465
lines changed

docs/guides/first-app.md renamed to docs/guides/first-app.mdx

Lines changed: 267 additions & 262 deletions
Large diffs are not rendered by default.

docusaurus.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ module.exports = {
7676
},
7777
},
7878
plugins: [
79+
'./webpack-fallbacks-plugin',
7980
[
8081
'@docusaurus/plugin-client-redirects',
8182
{

package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@
1212
"clear": "docusaurus clear"
1313
},
1414
"dependencies": {
15-
"@docusaurus/core": "2.0.0-beta.8",
16-
"@docusaurus/module-type-aliases": "^2.0.0-beta.8",
17-
"@docusaurus/plugin-client-redirects": "^2.0.0-beta.8",
18-
"@docusaurus/preset-classic": "2.0.0-beta.8",
15+
"@docusaurus/core": "^2.0.0-beta.15",
16+
"@docusaurus/module-type-aliases": "^2.0.0-beta.15",
17+
"@docusaurus/plugin-client-redirects": "^2.0.0-beta.15",
18+
"@docusaurus/preset-classic": "^2.0.0-beta.15",
1919
"@mdx-js/react": "^1.6.21",
2020
"amplitude-js": "^8.9.1",
2121
"clsx": "^1.1.1",
2222
"react": "^17.0.2",
2323
"react-cookie": "^4.1.1",
2424
"react-dom": "^17.0.2",
25-
"string.prototype.replaceall": "^1.0.6"
25+
"string.prototype.replaceall": "^1.0.6",
26+
"twig": "^1.15.4"
2627
},
2728
"resolutions": {
2829
"*/ua-parser-js": "0.7.24"
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import React, { PropsWithChildren, useContext, useMemo, useState } from "react";
2+
import { CookiesProvider, useCookies } from 'react-cookie';
3+
import * as replaceAll from 'string.prototype.replaceall';
4+
import CodeBlock from '@theme/CodeBlock';
5+
import Twig from 'twig';
6+
import { useDebouncedChecker } from "./debouncer";
7+
8+
enum InstanceKind {
9+
AUTHZED = 'authzed',
10+
SPICEDB = 'spicedb',
11+
}
12+
13+
type SampleConfigContextValue = {
14+
readonly defaultTenant: string,
15+
readonly instanceKind: () => InstanceKind | undefined,
16+
readonly setInstanceKind: (kind: InstanceKind) => void,
17+
readonly token: () => string | undefined;
18+
readonly setToken: (token: string) => void;
19+
readonly tenant: () => string | undefined;
20+
readonly setTenant: (token: string) => void;
21+
readonly endpoint: () => string | undefined;
22+
readonly setEndpoint: (token: string) => void;
23+
readonly buildTempalte: (template: string) => any;
24+
};
25+
26+
const SampleConfigContext = React.createContext<SampleConfigContextValue | undefined>(undefined);
27+
const DEFAULT_SAMPLE_TOKEN = 't_your_token_here_1234567deadbeef';
28+
const DEFAULT_ENDPOINT = 'localhost:50051';
29+
const AUTHZED_ENDPOINT = 'grpc.authzed.com:443';
30+
31+
/**
32+
* SampleConfigProvider is a React context provider which holds state for SampleCodeBlocks.
33+
*/
34+
export function SampleConfigProvider(props: PropsWithChildren<{ defaultTenant: string }>): JSX.Element {
35+
const [tenant, setTenant] = useState<string | undefined>(undefined);
36+
const [token, setToken] = useState<string | undefined>(undefined);
37+
const [endpoint, setEndpoint] = useState<string | undefined>(undefined);
38+
const [instanceKind, setInstanceKind] = useState<InstanceKind | undefined>(undefined);
39+
const [templateCache, setTemplateCache] = useState<Record<string, any>>({});
40+
41+
const contextValue = useMemo(() => {
42+
return {
43+
defaultTenant: props.defaultTenant,
44+
token: () => token,
45+
setToken: setToken,
46+
instanceKind: () => instanceKind,
47+
setInstanceKind: setInstanceKind,
48+
tenant: () => tenant,
49+
setTenant: setTenant,
50+
endpoint: () => endpoint,
51+
setEndpoint: setEndpoint,
52+
buildTemplate: (template: string) => {
53+
if (template in templateCache) {
54+
return templateCache[template];
55+
}
56+
57+
const built = Twig.twig({
58+
data: template
59+
});
60+
templateCache[template] = built;
61+
setTemplateCache(templateCache);
62+
return built;
63+
}
64+
}
65+
}, [props.defaultTenant, token, setToken, endpoint, setEndpoint, instanceKind, setInstanceKind, tenant, setTenant, templateCache, setTemplateCache]);
66+
67+
return (
68+
<SampleConfigContext.Provider value={contextValue}>
69+
{props.children}
70+
</SampleConfigContext.Provider>
71+
);
72+
}
73+
74+
function normalizeContent(children: React.ReactNode[]): string {
75+
return Array.isArray(children)
76+
? children.map((child: React.ReactNode) => {
77+
if (React.isValidElement(child)) {
78+
return normalizeContent(child.props.children);
79+
} else {
80+
return child.toString()
81+
}
82+
}).join('')
83+
: (children as string);
84+
}
85+
86+
/**
87+
* SampleCodeBlock is a component which takes as its child a single *twig* template
88+
* string, to produce a CodeBlock with the values found in the SampleConfigContext.
89+
*
90+
* Exmaple:
91+
* <SampleCodeBlock lang="java">
92+
* {`class SomeClass {
93+
* ·
94+
* private int someField = 2;
95+
* ·
96+
* void DoSomething() { ... }
97+
* }`}
98+
* </SampleCodeBlock>
99+
*
100+
* NOTE the use of · on the otherwise blanks lines to ensure MDX properly parses the template
101+
* string.
102+
*/
103+
export function SampleCodeBlock(props: PropsWithChildren<{ lang: string }>) {
104+
const context = useContext(SampleConfigContext);
105+
const token = context.token() ? context.token() : DEFAULT_SAMPLE_TOKEN;
106+
const endpoint = context.instanceKind() !== InstanceKind.SPICEDB ? AUTHZED_ENDPOINT : (context.endpoint() ?? DEFAULT_ENDPOINT);
107+
108+
let tenant = context.tenant() ? context.tenant() : context.defaultTenant;
109+
tenant = tenant.replace('/', '');
110+
111+
let content = normalizeContent(props.children);
112+
113+
// NOTE: due to a bug in MDX, a completely blank line is interpreted as breaking up
114+
// the template string typically passed to SampleCodeBlock. Therefore, we support ·
115+
// for otherwise blank links, to allow for MDX parsing.
116+
content = replaceAll(content, '·', '');
117+
118+
const template = context.buildTemplate(content);
119+
const processed = template.render({ token: token, endpoint: endpoint, tenant: tenant, authzed: context.instanceKind() !== InstanceKind.SPICEDB });
120+
return <CodeBlock className={`language-${props.lang}`}>{processed}</CodeBlock>
121+
}
122+
123+
/**
124+
* SampleConfigEditor is the editor for the values stored in the SampleConfigContext.
125+
*/
126+
export function SampleConfigEditor() {
127+
const context = useContext(SampleConfigContext);
128+
const instanceKind = context.instanceKind();
129+
const [token, setToken] = useState(context.token() ?? '');
130+
const [tenant, setTenant] = useState(context.tenant() ?? '');
131+
const [endpoint, setEndpoint] = useState(context.endpoint() ?? '');
132+
133+
const debouncedUpdateToken = useDebouncedChecker(100, context.setToken);
134+
const debouncedUpdateTenant = useDebouncedChecker(100, context.setTenant);
135+
const debouncedUpdateEndpoint = useDebouncedChecker(100, context.setEndpoint);
136+
137+
const handleChangeToken = (e: React.ChangeEvent) => {
138+
setToken(e.target.value);
139+
debouncedUpdateToken.run(e.target.value);
140+
};
141+
142+
const handleChangeTenant = (e: React.ChangeEvent) => {
143+
setTenant(e.target.value);
144+
debouncedUpdateTenant.run(e.target.value);
145+
};
146+
147+
const handleChangeEndpoint = (e: React.ChangeEvent) => {
148+
setEndpoint(e.target.value);
149+
debouncedUpdateEndpoint.run(e.target.value);
150+
};
151+
152+
return <div className="sample-config-editor">
153+
<div className="system-options">
154+
<div className="system-option">
155+
<input type="radio" id="authzed" name="system" value="authzed"
156+
checked={instanceKind === InstanceKind.AUTHZED}
157+
onChange={() => context.setInstanceKind(InstanceKind.AUTHZED)} />
158+
<label for="authzed">I have an <a href="https://app.authzed.com" target="_blank">Authzed permissions system</a> created</label>
159+
</div>
160+
<div className="system-option">
161+
<input type="radio" id="spicedb" name="system" value="spicedb"
162+
checked={instanceKind === InstanceKind.SPICEDB}
163+
onChange={() => {
164+
setTenant(undefined);
165+
debouncedUpdateTenant.run('');
166+
context.setInstanceKind(InstanceKind.SPICEDB);
167+
}} />
168+
<label for="spicedb">I have a <a href="https://github.com/authzed/spicedb" target="_blank">SpiceDB instance</a> running</label>
169+
</div>
170+
</div>
171+
{instanceKind !== undefined &&
172+
<div className="system-parameters">
173+
{instanceKind === InstanceKind.SPICEDB && <div>
174+
<label>Endpoint</label>
175+
<input type="text" value={endpoint} onChange={handleChangeEndpoint} placeholder="localhost:50051" />
176+
<label>Preshared key</label>
177+
<input type="text" value={token} onChange={handleChangeToken} placeholder="Enter your preshared key" />
178+
</div>}
179+
{instanceKind === InstanceKind.AUTHZED && <div>
180+
<label>Permissions System Prefix</label>
181+
<input type="text" value={tenant} onChange={handleChangeTenant} placeholder="mypermissionssystem/" />
182+
<label>Token</label>
183+
<input type="text" value={token} onChange={handleChangeToken} placeholder="tc_some_token" />
184+
</div>}
185+
</div>
186+
}
187+
</div>;
188+
}

src/components/debouncer.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useRef } from "react";
2+
3+
enum CheckerStatus {
4+
SLEEPING = 0,
5+
SCHEDULED = 1,
6+
REQUESTED = 2,
7+
RUNNING = 3,
8+
}
9+
10+
interface CheckerState<T> {
11+
status: CheckerStatus,
12+
runIndex: number
13+
argument: T | undefined
14+
}
15+
16+
/**
17+
* useDebouncedChecker is a hook for invoking a specific checker function after the given
18+
* rate of ms, and automatically handling debouncing and rechecking.
19+
*/
20+
export function useDebouncedChecker<T>(rate: number, checker: (arg: T) => Promise<void>) {
21+
const state = useRef<CheckerState<T>>({
22+
status: CheckerStatus.SLEEPING,
23+
runIndex: -1,
24+
argument: undefined
25+
});
26+
27+
const runChecker = (existingIndex: number) => {
28+
const arg = state.current.argument;
29+
if (arg === undefined) {
30+
return;
31+
}
32+
33+
const currentIndex = state.current.runIndex;
34+
if (currentIndex > existingIndex) {
35+
// Things are being changed rapidly. Wait until they are done.
36+
setTimeout(() => runChecker(currentIndex), rate);
37+
return;
38+
}
39+
40+
(async () => {
41+
state.current = {
42+
status: CheckerStatus.RUNNING,
43+
runIndex: existingIndex,
44+
argument: arg
45+
};
46+
await checker(arg);
47+
48+
// If the run went stale, issue another call.
49+
const nextIndex = state.current.runIndex;
50+
if (nextIndex > existingIndex) {
51+
setTimeout(() => runChecker(nextIndex), rate);
52+
return;
53+
}
54+
55+
state.current = {
56+
status: CheckerStatus.SLEEPING,
57+
runIndex: existingIndex,
58+
argument: undefined
59+
};
60+
})();
61+
};
62+
63+
return {
64+
run: (arg: T) => {
65+
// To prevent it blocking the main thread.
66+
(async () => {
67+
if (state.current.status === CheckerStatus.SLEEPING) {
68+
const currentIndex = state.current.runIndex + 1;
69+
state.current = {
70+
status: CheckerStatus.SCHEDULED,
71+
runIndex: currentIndex,
72+
argument: arg,
73+
}
74+
75+
// Kick off the timeout.
76+
setTimeout(() => runChecker(currentIndex), rate);
77+
} else {
78+
state.current = {
79+
...state.current,
80+
runIndex: state.current.runIndex + 1,
81+
argument: arg
82+
};
83+
};
84+
})();
85+
},
86+
isActive: () => {
87+
return state.current.status !== CheckerStatus.SLEEPING
88+
}
89+
};
90+
}

src/css/custom.css

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,47 @@ table td {
162162
height: 4rem;
163163
margin-bottom: 1rem;
164164
}
165+
166+
.sample-config-editor {
167+
padding: 10px;
168+
border: 1px solid #494b6c;
169+
}
170+
171+
.sample-config-editor .system-options {
172+
}
173+
174+
.sample-config-editor .system-parameters {
175+
margin-top: 10px;
176+
}
177+
178+
.sample-config-editor .system-parameters label {
179+
margin-bottom: 4px;
180+
font-size: 85%;
181+
margin-top: 10px;
182+
display: block;
183+
}
184+
185+
.sample-config-editor .system-parameters label:first-child {
186+
margin-top: 0px;
187+
}
188+
189+
.sample-config-editor .system-parameters input {
190+
padding: 8px;
191+
border-radius: 6px;
192+
border: 1px solid #ccc;
193+
display: block;
194+
width: 100%;
195+
font-size: 105%;
196+
}
197+
198+
.sample-config-editor .system-option {
199+
display: flex;
200+
align-items: center;
201+
}
202+
203+
.sample-config-editor input[type="radio"] {
204+
width: 1.2em;
205+
height: 1.2em;
206+
margin: 0px;
207+
margin-right: 6px;
208+
}

webpack-fallbacks-plugin.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
function fallbacksPlugin(
2+
context,
3+
opts,
4+
) {
5+
return {
6+
name: 'webpack-fallbacks-plugin',
7+
configureWebpack(config, isServer, utils, content) {
8+
return {
9+
resolve: {
10+
alias: {
11+
// path: require.resolve('path-browserify'),
12+
},
13+
fallback: {
14+
path: false
15+
},
16+
},
17+
};
18+
},
19+
};
20+
}
21+
22+
module.exports = fallbacksPlugin;

0 commit comments

Comments
 (0)