collabmd 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -7
- package/bin/collabmd.js +3 -2
- package/docker-compose.yml +13 -1
- package/package.json +2 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/{chunk-HEWVH67U.js → chunk-CYK7KO7W.js} +8 -8
- package/public/assets/js/chunks/editor-session-S5AT7BMV.js +22 -0
- package/public/assets/js/{excalidraw-editor-TCIH6GJL.js → excalidraw-editor-5YWMGLKF.js} +145 -62
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +503 -182
- package/public/index.html +104 -84
- package/src/client/application/app-shell/git-feature.js +247 -9
- package/src/client/application/app-shell/ui-feature.js +65 -7
- package/src/client/application/app-shell/workspace-feature.js +46 -0
- package/src/client/application/app-shell-elements.js +2 -0
- package/src/client/application/mermaid-preview-hydrator.js +1 -1
- package/src/client/application/plantuml-preview-hydrator.js +1 -1
- package/src/client/application/workspace-coordinator.js +147 -18
- package/src/client/application/workspace-preview-controller.js +34 -0
- package/src/client/application/workspace-route-controller.js +104 -0
- package/src/client/bootstrap/collabmd-app-shell.js +95 -0
- package/src/client/domain/runtime-paths.js +5 -0
- package/src/client/excalidraw-editor.js +50 -2
- package/src/client/infrastructure/auth-client.js +219 -52
- package/src/client/infrastructure/browser-navigation-port.js +20 -0
- package/src/client/infrastructure/editor-collaboration-client.js +4 -0
- package/src/client/infrastructure/editor-session.js +47 -7
- package/src/client/infrastructure/editor-view-adapter.js +122 -58
- package/src/client/infrastructure/runtime-config.js +66 -0
- package/src/client/presentation/comment-ui-controller.js +82 -26
- package/src/client/presentation/comments-panel.js +2 -2
- package/src/client/presentation/excalidraw-embed-controller.js +60 -2
- package/src/client/presentation/file-history-view-controller.js +806 -0
- package/src/client/presentation/git-diff-view-controller.js +718 -121
- package/src/client/presentation/git-panel-controller.js +458 -66
- package/src/client/presentation/layout-controller.js +1 -1
- package/src/client/styles/components/actions.css +89 -0
- package/src/client/styles/components/feedback.css +43 -0
- package/src/client/styles/components/navigation.css +68 -0
- package/src/client/styles/components/scrollbars.css +104 -0
- package/src/client/styles/components/states.css +289 -0
- package/src/client/styles/components/surfaces.css +155 -0
- package/src/client/styles/features/backlinks.css +213 -0
- package/src/client/styles/features/collaboration-chat.css +228 -0
- package/src/client/styles/features/collaboration-dialogs.css +154 -0
- package/src/client/styles/features/collaboration-presence.css +103 -0
- package/src/client/styles/features/collaboration-ui.css +3 -0
- package/src/client/styles/features/comment-card.css +236 -0
- package/src/client/styles/features/comment-markdown.css +127 -0
- package/src/client/styles/features/comment-overlays.css +60 -0
- package/src/client/styles/features/comments-drawer.css +120 -0
- package/src/client/styles/features/comments.css +4 -0
- package/src/client/styles/features/diagram-preview.css +227 -0
- package/src/client/styles/features/diff-commit.css +268 -0
- package/src/client/styles/features/diff-lines.css +160 -0
- package/src/client/styles/features/diff-toolbar.css +98 -0
- package/src/client/styles/features/diff.css +3 -0
- package/src/client/styles/features/embedded-preview.css +218 -0
- package/src/client/styles/features/git-branch.css +155 -0
- package/src/client/styles/features/git-files.css +105 -0
- package/src/client/styles/features/git-history.css +339 -0
- package/src/client/styles/features/git.css +3 -0
- package/src/client/styles/features/media-lightbox.css +157 -0
- package/src/client/styles/features/outline.css +115 -0
- package/src/client/styles/features/preview-content.css +3 -0
- package/src/client/styles/features/preview-markdown.css +201 -0
- package/src/client/styles/features/preview-sidebars.css +2 -0
- package/src/client/styles/features/quick-switcher.css +167 -0
- package/src/client/styles/foundation/themes.css +347 -0
- package/src/client/styles/foundation/tokens.css +66 -0
- package/src/client/styles/foundation/utilities.css +36 -0
- package/src/client/styles/layout/editor-page.css +203 -0
- package/src/client/styles/layout/overlays.css +132 -0
- package/src/client/styles/layout/responsive.css +251 -0
- package/src/client/styles/layout/shell.css +314 -0
- package/src/client/styles/layout/sidebar.css +176 -0
- package/src/client/styles/layout/view-modes.css +92 -0
- package/src/client/styles/overrides/codemirror.css +53 -0
- package/src/client/styles/overrides/highlightjs.css +7 -0
- package/src/client/styles/overrides/vendor.css +2 -0
- package/src/client/styles/primitives/controls.css +276 -0
- package/src/client/styles/style.css +25 -4945
- package/src/server/auth/create-auth-service.js +373 -15
- package/src/server/auth/session-cookie.js +30 -4
- package/src/server/config/env.js +121 -5
- package/src/server/create-app-server.js +8 -0
- package/src/server/domain/collaboration/collaboration-room.js +90 -47
- package/src/server/index.js +3 -1
- package/src/server/infrastructure/git/command-runner.js +2 -1
- package/src/server/infrastructure/git/git-service.js +48 -2
- package/src/server/infrastructure/git/history-service.js +683 -0
- package/src/server/infrastructure/git/parsers.js +30 -0
- package/src/server/infrastructure/git/responses.js +73 -0
- package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
- package/src/server/infrastructure/http/create-git-api-command-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
- package/src/server/infrastructure/http/create-request-handler.js +15 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.js +0 -22
|
@@ -317,6 +317,12 @@ function applyLocalScene(scene, {
|
|
|
317
317
|
...buildApiSceneUpdate(normalizedScene),
|
|
318
318
|
captureUpdate,
|
|
319
319
|
});
|
|
320
|
+
|
|
321
|
+
if (suppressOnChange) {
|
|
322
|
+
roomClient.commitSceneJson(normalizedJson, {
|
|
323
|
+
origin: 'excalidraw-local-scene-apply',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
320
326
|
}
|
|
321
327
|
|
|
322
328
|
function onRoomTextUpdate() {
|
|
@@ -469,9 +475,41 @@ function disconnectRealtimeRoom() {
|
|
|
469
475
|
roomClient.disconnect();
|
|
470
476
|
}
|
|
471
477
|
|
|
472
|
-
|
|
478
|
+
let didDisconnectRealtimeRoom = false;
|
|
479
|
+
|
|
480
|
+
function disconnectRealtimeRoomOnce() {
|
|
481
|
+
if (didDisconnectRealtimeRoom) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
didDisconnectRealtimeRoom = true;
|
|
473
486
|
disconnectRealtimeRoom();
|
|
474
|
-
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function waitForPendingRoomWrites({
|
|
490
|
+
intervalMs = 10,
|
|
491
|
+
maxWaitMs = 150,
|
|
492
|
+
} = {}) {
|
|
493
|
+
const startedAt = performance.now();
|
|
494
|
+
|
|
495
|
+
while ((performance.now() - startedAt) < maxWaitMs) {
|
|
496
|
+
const ws = roomClient.provider?.ws;
|
|
497
|
+
if (!ws || ws.readyState !== WebSocket.OPEN || ws.bufferedAmount === 0) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
await new Promise((resolve) => window.setTimeout(resolve, intervalMs));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function prepareRealtimeRoomDisconnect() {
|
|
506
|
+
roomClient.flushSceneSync();
|
|
507
|
+
await waitForPendingRoomWrites();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
window.addEventListener('pagehide', disconnectRealtimeRoomOnce);
|
|
511
|
+
window.addEventListener('beforeunload', disconnectRealtimeRoomOnce);
|
|
512
|
+
window.addEventListener('unload', disconnectRealtimeRoomOnce);
|
|
475
513
|
|
|
476
514
|
window.addEventListener('message', (event) => {
|
|
477
515
|
if (event.origin !== parentOrigin) {
|
|
@@ -503,6 +541,16 @@ window.addEventListener('message', (event) => {
|
|
|
503
541
|
|
|
504
542
|
if (message.type === 'follow-user') {
|
|
505
543
|
applyHostFollowRequest(message.peerId || null);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (message.type === 'prepare-disconnect') {
|
|
548
|
+
void (async () => {
|
|
549
|
+
await prepareRealtimeRoomDisconnect();
|
|
550
|
+
postToParent('disconnect-ready', {
|
|
551
|
+
requestId: message.requestId || '',
|
|
552
|
+
});
|
|
553
|
+
})();
|
|
506
554
|
}
|
|
507
555
|
});
|
|
508
556
|
|
|
@@ -3,7 +3,9 @@ import { getRuntimeConfig } from './runtime-config.js';
|
|
|
3
3
|
const DEFAULT_AUTH_CONFIG = {
|
|
4
4
|
enabled: false,
|
|
5
5
|
implemented: true,
|
|
6
|
+
loginEndpoint: '/api/auth/oidc/login',
|
|
6
7
|
passwordLabel: 'Password',
|
|
8
|
+
provider: '',
|
|
7
9
|
requiresLogin: false,
|
|
8
10
|
sessionEndpoint: '/api/auth/session',
|
|
9
11
|
statusEndpoint: '/api/auth/status',
|
|
@@ -11,6 +13,8 @@ const DEFAULT_AUTH_CONFIG = {
|
|
|
11
13
|
submitLabel: 'Continue',
|
|
12
14
|
};
|
|
13
15
|
|
|
16
|
+
const GOOGLE_G_LOGO_DATA_URL = 'data:image/png;base64,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';
|
|
17
|
+
|
|
14
18
|
let authGateStylesInjected = false;
|
|
15
19
|
|
|
16
20
|
function ensureAuthGateStyles() {
|
|
@@ -29,99 +33,182 @@ function ensureAuthGateStyles() {
|
|
|
29
33
|
justify-content: center;
|
|
30
34
|
padding: 24px;
|
|
31
35
|
background:
|
|
32
|
-
radial-gradient(circle at
|
|
33
|
-
rgba(
|
|
34
|
-
|
|
36
|
+
radial-gradient(circle at 50% 8%, rgba(37, 99, 235, 0.18), transparent 28%),
|
|
37
|
+
radial-gradient(circle at 60% 45%, rgba(14, 165, 233, 0.08), transparent 18%),
|
|
38
|
+
linear-gradient(180deg, rgba(8, 12, 24, 0.9), rgba(6, 10, 22, 0.96));
|
|
39
|
+
backdrop-filter: blur(18px);
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
.auth-gate-card {
|
|
38
|
-
width: min(100%,
|
|
39
|
-
padding:
|
|
40
|
-
border-radius:
|
|
41
|
-
border: 1px solid rgba(148, 163, 184, 0.
|
|
42
|
-
background:
|
|
43
|
+
width: min(100%, 440px);
|
|
44
|
+
padding: 32px;
|
|
45
|
+
border-radius: 24px;
|
|
46
|
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
47
|
+
background:
|
|
48
|
+
linear-gradient(180deg, rgba(17, 24, 39, 0.96), rgba(12, 18, 32, 0.96));
|
|
43
49
|
color: #e2e8f0;
|
|
44
|
-
box-shadow:
|
|
45
|
-
|
|
50
|
+
box-shadow:
|
|
51
|
+
0 28px 90px rgba(15, 23, 42, 0.42),
|
|
52
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
|
53
|
+
font-family: "Geist", "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
.auth-gate-card h1 {
|
|
49
57
|
margin: 0 0 10px;
|
|
50
|
-
font-size:
|
|
51
|
-
line-height: 1.
|
|
58
|
+
font-size: 2rem;
|
|
59
|
+
line-height: 1.05;
|
|
60
|
+
letter-spacing: -0.04em;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
.auth-gate-card p {
|
|
55
|
-
margin: 0 0
|
|
64
|
+
margin: 0 0 22px;
|
|
56
65
|
color: #cbd5e1;
|
|
57
66
|
line-height: 1.5;
|
|
67
|
+
font-size: 1rem;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
.auth-gate-form {
|
|
61
71
|
display: grid;
|
|
62
|
-
gap:
|
|
72
|
+
gap: 14px;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
.auth-gate-label {
|
|
66
76
|
display: grid;
|
|
67
|
-
gap:
|
|
77
|
+
gap: 10px;
|
|
68
78
|
font-size: 0.92rem;
|
|
79
|
+
font-weight: 500;
|
|
69
80
|
color: #cbd5e1;
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
.auth-gate-input {
|
|
73
84
|
width: 100%;
|
|
74
|
-
padding:
|
|
85
|
+
padding: 14px 16px;
|
|
75
86
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
76
|
-
border-radius:
|
|
87
|
+
border-radius: 16px;
|
|
77
88
|
background: rgba(15, 23, 42, 0.72);
|
|
78
89
|
color: inherit;
|
|
79
90
|
font: inherit;
|
|
91
|
+
box-sizing: border-box;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
.auth-gate-input:focus {
|
|
83
|
-
outline:
|
|
84
|
-
|
|
95
|
+
outline: none;
|
|
96
|
+
border-color: rgba(56, 189, 248, 0.72);
|
|
97
|
+
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.12);
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
.auth-gate-actions {
|
|
88
|
-
display:
|
|
89
|
-
align-items: center;
|
|
90
|
-
justify-content: space-between;
|
|
101
|
+
display: grid;
|
|
91
102
|
gap: 12px;
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
.auth-gate-button,
|
|
95
106
|
.auth-gate-secondary-button {
|
|
96
107
|
border: 0;
|
|
97
|
-
border-radius:
|
|
108
|
+
border-radius: 16px;
|
|
98
109
|
font: inherit;
|
|
99
110
|
cursor: pointer;
|
|
111
|
+
transition:
|
|
112
|
+
transform 160ms ease,
|
|
113
|
+
box-shadow 160ms ease,
|
|
114
|
+
border-color 160ms ease,
|
|
115
|
+
background-color 160ms ease;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.auth-gate-button:hover,
|
|
119
|
+
.auth-gate-secondary-button:hover {
|
|
120
|
+
transform: translateY(-1px);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.auth-gate-button:focus-visible,
|
|
124
|
+
.auth-gate-secondary-button:focus-visible {
|
|
125
|
+
outline: 3px solid rgba(125, 211, 252, 0.3);
|
|
126
|
+
outline-offset: 2px;
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
.auth-gate-button {
|
|
103
|
-
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
gap: 14px;
|
|
134
|
+
width: 100%;
|
|
135
|
+
padding: 14px 18px;
|
|
104
136
|
background: linear-gradient(135deg, #0f766e, #0284c7);
|
|
105
137
|
color: #f8fafc;
|
|
106
138
|
font-weight: 600;
|
|
139
|
+
box-shadow: 0 16px 40px rgba(2, 132, 199, 0.24);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.auth-gate-button:hover {
|
|
143
|
+
box-shadow: 0 18px 44px rgba(2, 132, 199, 0.28);
|
|
107
144
|
}
|
|
108
145
|
|
|
109
146
|
.auth-gate-secondary-button {
|
|
110
|
-
padding:
|
|
111
|
-
background:
|
|
112
|
-
color: #
|
|
147
|
+
padding: 13px 16px;
|
|
148
|
+
background: rgba(15, 23, 42, 0.48);
|
|
149
|
+
color: #cbd5e1;
|
|
150
|
+
border: 1px solid rgba(148, 163, 184, 0.22);
|
|
113
151
|
}
|
|
114
152
|
|
|
115
153
|
.auth-gate-button[disabled],
|
|
116
154
|
.auth-gate-secondary-button[disabled] {
|
|
117
155
|
opacity: 0.7;
|
|
118
156
|
cursor: wait;
|
|
157
|
+
transform: none;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.auth-gate-button--google {
|
|
161
|
+
background: rgba(255, 255, 255, 0.98);
|
|
162
|
+
color: #111827;
|
|
163
|
+
border: 1px solid rgba(148, 163, 184, 0.22);
|
|
164
|
+
box-shadow:
|
|
165
|
+
0 22px 40px rgba(15, 23, 42, 0.2),
|
|
166
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.65);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.auth-gate-button--google:hover {
|
|
170
|
+
background: #ffffff;
|
|
171
|
+
box-shadow:
|
|
172
|
+
0 24px 44px rgba(15, 23, 42, 0.24),
|
|
173
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.auth-gate-button__icon {
|
|
177
|
+
display: inline-flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
flex: 0 0 auto;
|
|
181
|
+
width: 22px;
|
|
182
|
+
height: 22px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.auth-gate-button__icon img {
|
|
186
|
+
display: block;
|
|
187
|
+
width: 100%;
|
|
188
|
+
height: 100%;
|
|
189
|
+
object-fit: contain;
|
|
119
190
|
}
|
|
120
191
|
|
|
121
192
|
.auth-gate-error {
|
|
122
193
|
min-height: 1.25rem;
|
|
123
194
|
color: #fca5a5;
|
|
124
195
|
font-size: 0.92rem;
|
|
196
|
+
margin-bottom: 4px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@media (max-width: 640px) {
|
|
200
|
+
.auth-gate-overlay {
|
|
201
|
+
padding: 16px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.auth-gate-card {
|
|
205
|
+
padding: 24px;
|
|
206
|
+
border-radius: 20px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.auth-gate-card h1 {
|
|
210
|
+
font-size: 1.7rem;
|
|
211
|
+
}
|
|
125
212
|
}
|
|
126
213
|
`;
|
|
127
214
|
|
|
@@ -159,6 +246,24 @@ function removePasswordFromHash() {
|
|
|
159
246
|
window.history.replaceState(null, '', nextUrl);
|
|
160
247
|
}
|
|
161
248
|
|
|
249
|
+
function consumeHashParam(name) {
|
|
250
|
+
const params = getHashParams();
|
|
251
|
+
const value = params.get(name) || '';
|
|
252
|
+
if (!value) {
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
params.delete(name);
|
|
257
|
+
const nextHash = params.toString();
|
|
258
|
+
const nextUrl = `${window.location.pathname}${window.location.search}${nextHash ? `#${nextHash}` : ''}`;
|
|
259
|
+
window.history.replaceState(null, '', nextUrl);
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getCurrentReturnTo() {
|
|
264
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
162
267
|
async function fetchAuthStatus(config) {
|
|
163
268
|
const response = await fetch(config.statusEndpoint, {
|
|
164
269
|
headers: {
|
|
@@ -174,6 +279,24 @@ async function fetchAuthStatus(config) {
|
|
|
174
279
|
return payload;
|
|
175
280
|
}
|
|
176
281
|
|
|
282
|
+
function syncOidcIdentityToLocalState(status) {
|
|
283
|
+
const authConfig = status?.auth ?? {};
|
|
284
|
+
if (authConfig.strategy !== 'oidc' || authConfig.provider !== 'google') {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const userName = String(status?.user?.name ?? '').trim();
|
|
289
|
+
if (!userName) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
window.localStorage.setItem('collabmd-user-name', userName);
|
|
295
|
+
} catch {
|
|
296
|
+
// Ignore storage errors.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
177
300
|
async function submitPassword(config, password) {
|
|
178
301
|
const response = await fetch(config.sessionEndpoint, {
|
|
179
302
|
method: 'POST',
|
|
@@ -204,6 +327,53 @@ function createOverlayShell() {
|
|
|
204
327
|
return { card, overlay };
|
|
205
328
|
}
|
|
206
329
|
|
|
330
|
+
function createGoogleIcon() {
|
|
331
|
+
const icon = document.createElement('span');
|
|
332
|
+
icon.className = 'auth-gate-button__icon';
|
|
333
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
334
|
+
const image = document.createElement('img');
|
|
335
|
+
image.alt = '';
|
|
336
|
+
image.decoding = 'async';
|
|
337
|
+
image.height = 20;
|
|
338
|
+
image.loading = 'eager';
|
|
339
|
+
image.src = GOOGLE_G_LOGO_DATA_URL;
|
|
340
|
+
image.width = 20;
|
|
341
|
+
icon.append(image);
|
|
342
|
+
return icon;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function renderOidcPrompt(card, config, {
|
|
346
|
+
errorMessage = '',
|
|
347
|
+
}) {
|
|
348
|
+
card.replaceChildren();
|
|
349
|
+
|
|
350
|
+
const heading = document.createElement('h1');
|
|
351
|
+
heading.textContent = 'Authentication required';
|
|
352
|
+
|
|
353
|
+
const copy = document.createElement('p');
|
|
354
|
+
copy.textContent = 'Sign in with Google to join this session.';
|
|
355
|
+
|
|
356
|
+
const actions = document.createElement('div');
|
|
357
|
+
actions.className = 'auth-gate-actions';
|
|
358
|
+
|
|
359
|
+
const signInButton = document.createElement('button');
|
|
360
|
+
signInButton.className = 'auth-gate-button auth-gate-button--google';
|
|
361
|
+
signInButton.type = 'button';
|
|
362
|
+
signInButton.append(createGoogleIcon(), document.createTextNode(config.submitLabel || 'Continue with Google'));
|
|
363
|
+
|
|
364
|
+
const error = document.createElement('div');
|
|
365
|
+
error.className = 'auth-gate-error';
|
|
366
|
+
error.textContent = errorMessage;
|
|
367
|
+
|
|
368
|
+
actions.append(signInButton);
|
|
369
|
+
card.append(heading, copy, error, actions);
|
|
370
|
+
signInButton.addEventListener('click', () => {
|
|
371
|
+
const loginUrl = new URL(config.loginEndpoint, window.location.origin);
|
|
372
|
+
loginUrl.searchParams.set('returnTo', getCurrentReturnTo());
|
|
373
|
+
window.location.assign(loginUrl.toString());
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
207
377
|
function renderStatusCard(card, {
|
|
208
378
|
title,
|
|
209
379
|
body,
|
|
@@ -234,7 +404,6 @@ function renderStatusCard(card, {
|
|
|
234
404
|
|
|
235
405
|
function renderPasswordPrompt(card, config, {
|
|
236
406
|
onSubmit,
|
|
237
|
-
onRetryStatus,
|
|
238
407
|
}) {
|
|
239
408
|
card.replaceChildren();
|
|
240
409
|
|
|
@@ -264,31 +433,21 @@ function renderPasswordPrompt(card, config, {
|
|
|
264
433
|
const actions = document.createElement('div');
|
|
265
434
|
actions.className = 'auth-gate-actions';
|
|
266
435
|
|
|
267
|
-
const retryButton = document.createElement('button');
|
|
268
|
-
retryButton.className = 'auth-gate-secondary-button';
|
|
269
|
-
retryButton.type = 'button';
|
|
270
|
-
retryButton.textContent = 'Retry status';
|
|
271
|
-
|
|
272
436
|
const submitButton = document.createElement('button');
|
|
273
437
|
submitButton.className = 'auth-gate-button';
|
|
274
438
|
submitButton.type = 'submit';
|
|
275
439
|
submitButton.textContent = config.submitLabel;
|
|
276
440
|
|
|
277
|
-
actions.append(
|
|
441
|
+
actions.append(submitButton);
|
|
278
442
|
label.append(input);
|
|
279
443
|
form.append(heading, copy, label, error, actions);
|
|
280
444
|
card.append(form);
|
|
281
445
|
|
|
282
|
-
retryButton.addEventListener('click', () => {
|
|
283
|
-
void onRetryStatus();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
446
|
form.addEventListener('submit', (event) => {
|
|
287
447
|
event.preventDefault();
|
|
288
448
|
const password = input.value;
|
|
289
449
|
input.disabled = true;
|
|
290
450
|
submitButton.disabled = true;
|
|
291
|
-
retryButton.disabled = true;
|
|
292
451
|
error.textContent = '';
|
|
293
452
|
|
|
294
453
|
void onSubmit(password).catch((submissionError) => {
|
|
@@ -297,7 +456,6 @@ function renderPasswordPrompt(card, config, {
|
|
|
297
456
|
: 'Authentication failed';
|
|
298
457
|
input.disabled = false;
|
|
299
458
|
submitButton.disabled = false;
|
|
300
|
-
retryButton.disabled = false;
|
|
301
459
|
input.select();
|
|
302
460
|
input.focus();
|
|
303
461
|
});
|
|
@@ -316,11 +474,17 @@ export async function ensureClientAuthenticated() {
|
|
|
316
474
|
|
|
317
475
|
const { card, overlay } = createOverlayShell();
|
|
318
476
|
document.body.append(overlay);
|
|
477
|
+
let pendingOidcError = consumeHashParam('auth_error');
|
|
319
478
|
return new Promise((resolve) => {
|
|
320
|
-
const resolveAuthenticated = () => {
|
|
479
|
+
const resolveAuthenticated = (status = null) => {
|
|
321
480
|
removePasswordFromHash();
|
|
481
|
+
syncOidcIdentityToLocalState(status);
|
|
322
482
|
overlay.remove();
|
|
323
|
-
resolve({
|
|
483
|
+
resolve({
|
|
484
|
+
authenticated: true,
|
|
485
|
+
auth: status?.auth ?? config,
|
|
486
|
+
user: status?.user ?? null,
|
|
487
|
+
});
|
|
324
488
|
};
|
|
325
489
|
|
|
326
490
|
const verifyAccess = async () => {
|
|
@@ -332,15 +496,15 @@ export async function ensureClientAuthenticated() {
|
|
|
332
496
|
try {
|
|
333
497
|
const status = await fetchAuthStatus(config);
|
|
334
498
|
if (status.authenticated) {
|
|
335
|
-
resolveAuthenticated();
|
|
499
|
+
resolveAuthenticated(status);
|
|
336
500
|
return;
|
|
337
501
|
}
|
|
338
502
|
} catch (error) {
|
|
339
503
|
renderStatusCard(card, {
|
|
340
|
-
body: error instanceof Error
|
|
341
|
-
|
|
504
|
+
body: error instanceof Error
|
|
505
|
+
? `${error.message} Refresh the page and try again.`
|
|
506
|
+
: 'Failed to contact the auth service. Refresh the page and try again.',
|
|
342
507
|
title: 'Cannot verify access',
|
|
343
|
-
onSecondaryAction: verifyAccess,
|
|
344
508
|
});
|
|
345
509
|
return;
|
|
346
510
|
}
|
|
@@ -359,7 +523,6 @@ export async function ensureClientAuthenticated() {
|
|
|
359
523
|
}
|
|
360
524
|
|
|
361
525
|
renderPasswordPrompt(card, config, {
|
|
362
|
-
onRetryStatus: verifyAccess,
|
|
363
526
|
onSubmit: async (password) => {
|
|
364
527
|
await submitPassword(config, password);
|
|
365
528
|
resolveAuthenticated();
|
|
@@ -368,13 +531,17 @@ export async function ensureClientAuthenticated() {
|
|
|
368
531
|
return;
|
|
369
532
|
}
|
|
370
533
|
|
|
534
|
+
if (config.strategy === 'oidc') {
|
|
535
|
+
renderOidcPrompt(card, config, {
|
|
536
|
+
errorMessage: pendingOidcError,
|
|
537
|
+
});
|
|
538
|
+
pendingOidcError = '';
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
371
542
|
renderStatusCard(card, {
|
|
372
|
-
body:
|
|
373
|
-
? 'OIDC authentication is configured but not implemented in this build yet.'
|
|
374
|
-
: 'This authentication strategy is not available.',
|
|
375
|
-
secondaryActionLabel: 'Retry status',
|
|
543
|
+
body: 'This authentication strategy is not available.',
|
|
376
544
|
title: 'Authentication unavailable',
|
|
377
|
-
onSecondaryAction: verifyAccess,
|
|
378
545
|
});
|
|
379
546
|
};
|
|
380
547
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getHashRoute,
|
|
3
|
+
navigateToGitCommit,
|
|
3
4
|
navigateToFile,
|
|
4
5
|
navigateToGitDiff,
|
|
6
|
+
navigateToGitFileHistory,
|
|
7
|
+
navigateToGitFilePreview,
|
|
8
|
+
navigateToGitHistory,
|
|
5
9
|
} from './runtime-config.js';
|
|
6
10
|
|
|
7
11
|
export class BrowserNavigationPort {
|
|
@@ -16,4 +20,20 @@ export class BrowserNavigationPort {
|
|
|
16
20
|
navigateToGitDiff(payload) {
|
|
17
21
|
navigateToGitDiff(payload);
|
|
18
22
|
}
|
|
23
|
+
|
|
24
|
+
navigateToGitCommit(payload) {
|
|
25
|
+
navigateToGitCommit(payload);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
navigateToGitHistory() {
|
|
29
|
+
navigateToGitHistory();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
navigateToGitFileHistory(payload) {
|
|
33
|
+
navigateToGitFileHistory(payload);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
navigateToGitFilePreview(payload) {
|
|
37
|
+
navigateToGitFilePreview(payload);
|
|
38
|
+
}
|
|
19
39
|
}
|
|
@@ -125,6 +125,10 @@ export class EditorCollaborationClient {
|
|
|
125
125
|
return Promise.resolve();
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
if (timeoutMs === null || timeoutMs === undefined || timeoutMs === false) {
|
|
129
|
+
return this.initialSyncPromise;
|
|
130
|
+
}
|
|
131
|
+
|
|
128
132
|
return Promise.race([
|
|
129
133
|
this.initialSyncPromise,
|
|
130
134
|
new Promise((resolve) => {
|
|
@@ -24,6 +24,9 @@ export class EditorSession {
|
|
|
24
24
|
this.onCommentsChange = onCommentsChange;
|
|
25
25
|
this.onContentChange = onContentChange;
|
|
26
26
|
this.onSelectionChange = onSelectionChange;
|
|
27
|
+
this.activeFilePath = '';
|
|
28
|
+
this.bootstrapContent = null;
|
|
29
|
+
this.pendingCollaborativeBindings = null;
|
|
27
30
|
this.hasDeliveredContent = false;
|
|
28
31
|
this.lastDeliveredContent = null;
|
|
29
32
|
|
|
@@ -61,24 +64,58 @@ export class EditorSession {
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
async initialize(filePath) {
|
|
67
|
+
this.activeFilePath = filePath;
|
|
64
68
|
this.hasDeliveredContent = false;
|
|
65
69
|
this.lastDeliveredContent = null;
|
|
66
70
|
const collaborationBindings = await this.collaborationClient.initialize(filePath);
|
|
67
|
-
|
|
68
|
-
this.viewAdapter.initialize({
|
|
69
|
-
awareness: collaborationBindings.awareness,
|
|
70
|
-
filePath,
|
|
71
|
-
undoManager: collaborationBindings.undoManager,
|
|
72
|
-
ytext: collaborationBindings.ytext,
|
|
73
|
-
});
|
|
74
71
|
this.commentThreadStore.bind({
|
|
75
72
|
commentThreads: collaborationBindings.commentThreads,
|
|
76
73
|
ydoc: collaborationBindings.ydoc,
|
|
77
74
|
ytext: collaborationBindings.ytext,
|
|
78
75
|
});
|
|
76
|
+
|
|
77
|
+
this.pendingCollaborativeBindings = collaborationBindings;
|
|
78
|
+
if (this.collaborationClient.initialSyncComplete) {
|
|
79
|
+
this.activateCollaborativeView();
|
|
80
|
+
}
|
|
81
|
+
|
|
79
82
|
this.onAwarenessChange?.(this.collaborationClient.collectUsers((cursor) => this.resolveAwarenessCursor(cursor)));
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
activateCollaborativeView() {
|
|
86
|
+
if (!this.pendingCollaborativeBindings) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.viewAdapter.initialize({
|
|
91
|
+
awareness: this.pendingCollaborativeBindings.awareness,
|
|
92
|
+
filePath: this.activeFilePath,
|
|
93
|
+
undoManager: this.pendingCollaborativeBindings.undoManager,
|
|
94
|
+
ytext: this.pendingCollaborativeBindings.ytext,
|
|
95
|
+
});
|
|
96
|
+
this.pendingCollaborativeBindings = null;
|
|
97
|
+
this.bootstrapContent = null;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
showBootstrapContent({ content = '', filePath = this.activeFilePath } = {}) {
|
|
102
|
+
if (this.collaborationClient.initialSyncComplete) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.activeFilePath = filePath;
|
|
107
|
+
this.bootstrapContent = String(content ?? '');
|
|
108
|
+
this.viewAdapter.initializeProvisional({
|
|
109
|
+
content: this.bootstrapContent,
|
|
110
|
+
filePath: this.activeFilePath,
|
|
111
|
+
});
|
|
112
|
+
return this.emitContentChange();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
hasBootstrapContent() {
|
|
116
|
+
return this.bootstrapContent !== null;
|
|
117
|
+
}
|
|
118
|
+
|
|
82
119
|
emitContentChange({ force = false } = {}) {
|
|
83
120
|
const nextContent = this.getText();
|
|
84
121
|
if (!force && this.hasDeliveredContent && nextContent === this.lastDeliveredContent) {
|
|
@@ -239,6 +276,9 @@ export class EditorSession {
|
|
|
239
276
|
|
|
240
277
|
destroy() {
|
|
241
278
|
this.commentThreadStore.unbind();
|
|
279
|
+
this.activeFilePath = '';
|
|
280
|
+
this.bootstrapContent = null;
|
|
281
|
+
this.pendingCollaborativeBindings = null;
|
|
242
282
|
this.hasDeliveredContent = false;
|
|
243
283
|
this.lastDeliveredContent = null;
|
|
244
284
|
this.collaborationClient.destroy();
|