Skip to content

Commit 50618b4

Browse files
committed
add JS animation to OAuth 2.0 User Authentication Security Evolution
1 parent f9176c3 commit 50618b4

File tree

6 files changed

+1278
-25
lines changed

6 files changed

+1278
-25
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ run:
2828
@echo "${BOLD}${YELLOW}mkdocs serve:${NORMAL}"
2929
uv run mkdocs serve --dirty
3030

31+
reload-oauth:
32+
@echo "${BOLD}${YELLOW}reload oauth animation assets:${NORMAL}"
33+
cp /home/xiang/git/copdips.github.io/docs/javascripts/oauth-evolution.js /home/xiang/git/copdips.github.io/site/javascripts/oauth-evolution.js && cp /home/xiang/git/copdips.github.io/docs/stylesheets/oauth-evolution.css /home/xiang/git/copdips.github.io/site/stylesheets/oauth-evolution.css && python3 /home/xiang/git/copdips.github.io/scripts/reload_page.py
34+
3135
update-venv:
3236
@echo "${BOLD}${YELLOW}update venv:${NORMAL}"
3337
# ${PYTHON} -m pip install -U pip
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
(function() {
2+
function createElement(tag, className, text) {
3+
const el = document.createElement(tag);
4+
if (className) el.className = className;
5+
if (text) el.textContent = text;
6+
return el;
7+
}
8+
9+
function renderOAuthEvolution(container, config) {
10+
const steps = config.steps || [];
11+
if (!steps.length) return;
12+
13+
const wrapper = createElement("div", "oauth-evolution-wrapper");
14+
15+
// Navigation container
16+
const nav = createElement("div", "oauth-evolution-nav");
17+
18+
// Left arrow
19+
const leftArrow = createElement("button", "oauth-nav-arrow");
20+
leftArrow.innerHTML = "←";
21+
leftArrow.setAttribute("aria-label", "Previous flow");
22+
23+
// Timeline container
24+
const timeline = createElement("div", "oauth-evolution-timeline");
25+
const timelineLine = createElement("div", "oauth-timeline-line");
26+
const timelineProgress = createElement("div", "oauth-timeline-progress");
27+
const timelineSteps = createElement("div", "oauth-timeline-steps");
28+
29+
timeline.appendChild(timelineLine);
30+
timeline.appendChild(timelineProgress);
31+
timeline.appendChild(timelineSteps);
32+
33+
// Right arrow
34+
const rightArrow = createElement("button", "oauth-nav-arrow");
35+
rightArrow.innerHTML = "→";
36+
rightArrow.setAttribute("aria-label", "Next flow");
37+
38+
nav.appendChild(leftArrow);
39+
nav.appendChild(timeline);
40+
nav.appendChild(rightArrow);
41+
42+
// Content area
43+
const content = createElement("div", "oauth-evolution-content");
44+
45+
wrapper.appendChild(nav);
46+
wrapper.appendChild(content);
47+
container.appendChild(wrapper);
48+
49+
let activeIndex = 0;
50+
51+
function updateProgress() {
52+
const progress = steps.length > 1 ? (activeIndex / (steps.length - 1)) * 100 : 0;
53+
timelineProgress.style.width = progress + '%';
54+
}
55+
56+
function updateArrows() {
57+
leftArrow.disabled = activeIndex === 0;
58+
rightArrow.disabled = activeIndex === steps.length - 1;
59+
}
60+
61+
function renderStep(index, withAnimation = true) {
62+
activeIndex = index;
63+
const step = steps[index];
64+
if (!step) return;
65+
66+
// Add transition class
67+
if (withAnimation) {
68+
content.classList.add('transitioning');
69+
}
70+
71+
setTimeout(() => {
72+
// Update timeline dots
73+
Array.from(timelineSteps.children).forEach((dot, i) => {
74+
if (i === index) {
75+
dot.classList.add("active");
76+
dot.classList.remove("completed");
77+
} else if (i < index) {
78+
dot.classList.remove("active");
79+
dot.classList.add("completed");
80+
} else {
81+
dot.classList.remove("active", "completed");
82+
}
83+
});
84+
85+
updateProgress();
86+
updateArrows();
87+
88+
// Clear and render content
89+
content.innerHTML = "";
90+
91+
// Title
92+
const title = createElement("h3", "oauth-flow-title", step.title);
93+
content.appendChild(title);
94+
95+
// Comparison badge if available
96+
if (step.comparedTo) {
97+
const comparisonDiv = document.createElement("div");
98+
comparisonDiv.style.textAlign = "center";
99+
const comparison = createElement("div", "oauth-comparison",
100+
`Evolution from: ${step.comparedTo}`);
101+
comparisonDiv.appendChild(comparison);
102+
content.appendChild(comparisonDiv);
103+
}
104+
105+
// Render sequence diagram
106+
if (step.actors && step.interactions) {
107+
const diagram = renderSequenceDiagram(step);
108+
content.appendChild(diagram);
109+
}
110+
111+
// Key improvement
112+
if (step.keyImprovement) {
113+
const improvement = createElement("div", "oauth-key-improvement");
114+
const improvementTitle = createElement("div", "oauth-key-improvement-title",
115+
"Key Security Improvement in " + step.title);
116+
const improvementText = createElement("div", "oauth-key-improvement-text",
117+
step.keyImprovement);
118+
improvement.appendChild(improvementTitle);
119+
improvement.appendChild(improvementText);
120+
content.appendChild(improvement);
121+
}
122+
123+
// Remove transition class
124+
if (withAnimation) {
125+
content.classList.remove('transitioning');
126+
}
127+
}, withAnimation ? 150 : 0);
128+
}
129+
130+
function renderSequenceDiagram(step) {
131+
const diagram = createElement("div", "oauth-sequence-diagram");
132+
133+
// Actors
134+
const actorsContainer = createElement("div", "oauth-actors");
135+
const actorElements = {};
136+
137+
step.actors.forEach((actorName, index) => {
138+
const actor = createElement("div", "oauth-actor");
139+
const actorBox = createElement("div", "oauth-actor-box", actorName);
140+
const lifeline = createElement("div", "oauth-lifeline");
141+
142+
actor.appendChild(actorBox);
143+
actor.appendChild(lifeline);
144+
actorsContainer.appendChild(actor);
145+
146+
actorElements[actorName] = { element: actor, index: index };
147+
});
148+
149+
diagram.appendChild(actorsContainer);
150+
151+
// Interactions
152+
const interactionsContainer = createElement("div", "oauth-interactions");
153+
154+
step.interactions.forEach((interaction, interactionIndex) => {
155+
const interactionDiv = createElement("div", "oauth-interaction");
156+
157+
if (interaction.highlight) {
158+
interactionDiv.classList.add("highlight");
159+
} else if (interaction.improvement) {
160+
interactionDiv.classList.add("improvement");
161+
} else if (interaction.fixes) {
162+
interactionDiv.classList.add("fixed");
163+
}
164+
165+
// Add step number badge
166+
const stepNumber = createElement("div", "oauth-step-number", (interactionIndex + 1).toString());
167+
interactionDiv.appendChild(stepNumber);
168+
169+
// Calculate arrow position
170+
const fromIndex = actorElements[interaction.from].index;
171+
const toIndex = actorElements[interaction.to].index;
172+
const numActors = step.actors.length;
173+
174+
// Arrow
175+
const arrow = createElement("div", "oauth-arrow");
176+
if (toIndex < fromIndex) {
177+
arrow.classList.add("reverse");
178+
}
179+
180+
// Store indices for later positioning adjustment
181+
arrow.dataset.fromIndex = fromIndex;
182+
arrow.dataset.toIndex = toIndex;
183+
arrow.dataset.numActors = numActors;
184+
185+
// Initial positioning (will be adjusted after render)
186+
const actorWidth = 100 / numActors;
187+
const fromPos = (fromIndex + 0.5) * actorWidth;
188+
const toPos = (toIndex + 0.5) * actorWidth;
189+
190+
if (toIndex > fromIndex) {
191+
arrow.style.left = fromPos + '%';
192+
arrow.style.width = (toPos - fromPos) + '%';
193+
} else {
194+
arrow.style.left = toPos + '%';
195+
arrow.style.width = (fromPos - toPos) + '%';
196+
}
197+
198+
// Arrow label with step number
199+
const labelText = (interactionIndex + 1) + '. ' + interaction.message;
200+
const label = createElement("div", "oauth-arrow-label", labelText);
201+
arrow.appendChild(label);
202+
203+
interactionDiv.appendChild(arrow);
204+
205+
// Add warning note if present
206+
if (interaction.warning) {
207+
const warningNote = createElement("div", "oauth-warning-note");
208+
const warningTitle = createElement("div", "oauth-warning-note-title", "Security Risk");
209+
const warningText = document.createTextNode(interaction.warning);
210+
211+
warningNote.appendChild(warningTitle);
212+
warningNote.appendChild(warningText);
213+
interactionDiv.appendChild(warningNote);
214+
215+
// Add animation delay
216+
warningNote.style.animationDelay = (0.2 + interactionIndex * 0.15 + 0.2) + 's';
217+
}
218+
219+
// Add fixed note if present
220+
if (interaction.fixes) {
221+
const fixedNote = createElement("div", "oauth-fixed-note");
222+
const fixedTitle = createElement("div", "oauth-fixed-note-title", "Fixed!");
223+
const fixedText = document.createTextNode(interaction.fixes);
224+
225+
fixedNote.appendChild(fixedTitle);
226+
fixedNote.appendChild(fixedText);
227+
interactionDiv.appendChild(fixedNote);
228+
229+
// Add animation delay
230+
fixedNote.style.animationDelay = (0.2 + interactionIndex * 0.15 + 0.2) + 's';
231+
}
232+
233+
interactionsContainer.appendChild(interactionDiv);
234+
});
235+
236+
diagram.appendChild(interactionsContainer);
237+
238+
// Fix lifeline heights and arrow positions after rendering
239+
setTimeout(() => {
240+
const diagramHeight = diagram.offsetHeight;
241+
const actorsHeight = actorsContainer.offsetHeight;
242+
const lifelineHeight = diagramHeight - actorsHeight;
243+
244+
actorsContainer.querySelectorAll('.oauth-lifeline').forEach(lifeline => {
245+
lifeline.style.height = lifelineHeight + 'px';
246+
});
247+
248+
// Adjust arrow positions to align with lifelines using absolute pixel positions
249+
const interactionsContainer = diagram.querySelector('.oauth-interactions');
250+
const interactionsLeft = interactionsContainer.getBoundingClientRect().left;
251+
const interactionsWidth = interactionsContainer.offsetWidth;
252+
253+
// Get actor positions
254+
const actors = actorsContainer.querySelectorAll('.oauth-actor');
255+
256+
diagram.querySelectorAll('.oauth-arrow').forEach(arrow => {
257+
const fromIndex = parseInt(arrow.dataset.fromIndex);
258+
const toIndex = parseInt(arrow.dataset.toIndex);
259+
260+
// Get the center position of each actor
261+
const fromActor = actors[fromIndex];
262+
const toActor = actors[toIndex];
263+
const fromActorRect = fromActor.getBoundingClientRect();
264+
const toActorRect = toActor.getBoundingClientRect();
265+
266+
const fromActorCenter = fromActorRect.left + fromActorRect.width / 2;
267+
const toActorCenter = toActorRect.left + toActorRect.width / 2;
268+
269+
// Convert to percentage of interactions container
270+
const fromPos = ((fromActorCenter - interactionsLeft) / interactionsWidth) * 100;
271+
const toPos = ((toActorCenter - interactionsLeft) / interactionsWidth) * 100;
272+
273+
if (toIndex > fromIndex) {
274+
arrow.style.left = fromPos + '%';
275+
arrow.style.width = (toPos - fromPos) + '%';
276+
} else {
277+
arrow.style.left = toPos + '%';
278+
arrow.style.width = (fromPos - toPos) + '%';
279+
}
280+
});
281+
}, 50);
282+
283+
return diagram;
284+
}
285+
286+
// Create timeline dots
287+
steps.forEach((step, index) => {
288+
const timelineStep = createElement("div", "oauth-timeline-step");
289+
290+
// Mark first 3 flows as deprecated
291+
if (index < 3) {
292+
timelineStep.classList.add("deprecated");
293+
}
294+
295+
const dot = createElement("div", "oauth-timeline-dot", (index + 1).toString());
296+
const label = createElement("div", "oauth-timeline-label", step.label);
297+
const fullTitle = createElement("div", "oauth-timeline-full-title", step.title);
298+
299+
timelineStep.appendChild(dot);
300+
timelineStep.appendChild(label);
301+
timelineStep.appendChild(fullTitle);
302+
303+
timelineStep.addEventListener("click", () => {
304+
renderStep(index);
305+
});
306+
307+
timelineSteps.appendChild(timelineStep);
308+
});
309+
310+
// Arrow navigation
311+
leftArrow.addEventListener("click", () => {
312+
if (activeIndex > 0) {
313+
renderStep(activeIndex - 1);
314+
}
315+
});
316+
317+
rightArrow.addEventListener("click", () => {
318+
if (activeIndex < steps.length - 1) {
319+
renderStep(activeIndex + 1);
320+
}
321+
});
322+
323+
// Keyboard navigation
324+
wrapper.addEventListener("keydown", (e) => {
325+
if (e.key === "ArrowLeft" && activeIndex > 0) {
326+
renderStep(activeIndex - 1);
327+
} else if (e.key === "ArrowRight" && activeIndex < steps.length - 1) {
328+
renderStep(activeIndex + 1);
329+
}
330+
});
331+
332+
// Initial render
333+
renderStep(0, false);
334+
}
335+
336+
function initAll() {
337+
document.querySelectorAll(".oauth-evolution").forEach(container => {
338+
try {
339+
const script = container.querySelector("script[type='application/json']");
340+
if (!script) return;
341+
const config = JSON.parse(script.textContent);
342+
container.innerHTML = "";
343+
renderOAuthEvolution(container, config);
344+
} catch (e) {
345+
console.error("Error initializing oauth-evolution:", e);
346+
}
347+
});
348+
}
349+
350+
// Initialize on page load
351+
if (document.readyState !== "loading") {
352+
initAll();
353+
} else {
354+
document.addEventListener("DOMContentLoaded", initAll);
355+
}
356+
357+
// Support MkDocs Material instant loading
358+
document.addEventListener("DOMContentLoaded", initAll);
359+
document.addEventListener("readystatechange", function() {
360+
if (document.readyState === "complete") {
361+
initAll();
362+
}
363+
});
364+
365+
// MkDocs Material navigation hook
366+
if (typeof document$ !== 'undefined') {
367+
document$.subscribe(() => {
368+
setTimeout(initAll, 100);
369+
});
370+
}
371+
})();

0 commit comments

Comments
 (0)