vimp-engine 0.37.0 → 0.38.0
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/package.json +1 -1
- package/src/client/boot.js +1 -1
- package/src/client/components/controller/LobbyAuth.js +97 -15
- package/src/client/components/controller/Profile.js +85 -0
- package/src/client/components/model/Games.js +1 -1
- package/src/client/components/model/LobbyAuth.js +344 -115
- package/src/client/components/model/Profile.js +142 -0
- package/src/client/components/view/LobbyAuth.js +150 -62
- package/src/client/components/view/Profile.js +373 -0
- package/src/client/lib/authApi.js +97 -0
- package/src/client/lib/pkce.js +23 -0
- package/src/client/lib/roomLink.js +1 -1
- package/src/client/lib/signalingErrors.js +3 -3
- package/src/client/lib/turnstile.js +130 -0
- package/src/client/main.js +89 -24
- package/src/client/network/TokenRenewer.js +90 -0
- package/src/client/session/GuestSession.js +79 -19
- package/src/client/style.css +119 -16
- package/src/client/views/includes/lobby.pug +11 -6
- package/src/client/views/includes/lobbyAuth.pug +34 -16
- package/src/client/views/includes/profile.pug +73 -0
- package/src/client/views/index.pug +1 -0
- package/src/config/authClient.js +54 -30
- package/src/config/chatMessages.js +1 -2
- package/src/config/env.js +2 -2
- package/src/config/lobby.js +50 -1
- package/src/config/master.js +15 -8
- package/src/config/wsports.js +4 -1
- package/src/devtools/builtinScenario.js +1 -1
- package/src/devtools/contract/rules/a2-package-scripts.js +1 -1
- package/src/devtools/contract/rules/a4-vite-config.js +1 -1
- package/src/devtools/contract/rules/b8-system-messages.js +1 -1
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +1 -1
- package/src/host/GameCoreAdapter.js +0 -3
- package/src/host/HostGame.js +28 -0
- package/src/host/PortMachine.js +50 -1
- package/src/host/meta/SocketManager.js +0 -13
- package/src/host/meta/core/RoundManager.js +0 -19
- package/src/host/meta/modules/PlayerDataSync.js +29 -3
- package/src/host/meta/modules/chat/systemMessages.js +1 -4
- package/src/lib/loadGamePackage.js +1 -1
- package/src/lib/validators.js +8 -0
- package/src/standalone/index.js +1 -1
|
@@ -21,6 +21,9 @@ import WebRtcManager from '../network/WebRtcManager.js';
|
|
|
21
21
|
// Без DOM: сигналинг, роли, супервизор, UI-действия и фабрики инъектирует
|
|
22
22
|
// сборщик (client/main.js).
|
|
23
23
|
|
|
24
|
+
// минимум между продлениями входа по одному виду запроса
|
|
25
|
+
const RENEW_RETRY_MIN_MS = 60000;
|
|
26
|
+
|
|
24
27
|
export default class GuestSession {
|
|
25
28
|
/**
|
|
26
29
|
* @param {Object} deps
|
|
@@ -32,7 +35,8 @@ export default class GuestSession {
|
|
|
32
35
|
* @param {Function} deps.getPromotion - () → PromotionFlow.
|
|
33
36
|
* @param {Function} deps.getSupervisor - () → SessionSupervisor | null.
|
|
34
37
|
* @param {Function} deps.getToken - () → identity-токен.
|
|
35
|
-
* @param {Function} deps.
|
|
38
|
+
* @param {Function} deps.refreshIdentity - () → Promise<'ok'|'retry'|'lost'>,
|
|
39
|
+
* продление identity-токена по сессии устройства.
|
|
36
40
|
* @param {Object} deps.config - lobby-конфиг (webrtc, reconnect).
|
|
37
41
|
* @param {Object} deps.diagnostics
|
|
38
42
|
* @param {Object} deps.ui - { ensureWebRtc() → bool, showRoomLink(roomId),
|
|
@@ -51,7 +55,7 @@ export default class GuestSession {
|
|
|
51
55
|
getPromotion,
|
|
52
56
|
getSupervisor,
|
|
53
57
|
getToken,
|
|
54
|
-
|
|
58
|
+
refreshIdentity,
|
|
55
59
|
config,
|
|
56
60
|
diagnostics,
|
|
57
61
|
ui,
|
|
@@ -67,7 +71,7 @@ export default class GuestSession {
|
|
|
67
71
|
this._getPromotion = getPromotion;
|
|
68
72
|
this._getSupervisor = getSupervisor;
|
|
69
73
|
this._getToken = getToken;
|
|
70
|
-
this.
|
|
74
|
+
this._refreshIdentity = refreshIdentity;
|
|
71
75
|
this._config = config;
|
|
72
76
|
this._diagnostics = diagnostics;
|
|
73
77
|
this._ui = ui;
|
|
@@ -83,6 +87,8 @@ export default class GuestSession {
|
|
|
83
87
|
// открытое у гостя голосование «Change host» ({ roomId, voteId }) —
|
|
84
88
|
// ответ окна уходит мастеру с этим voteId
|
|
85
89
|
this._hostVote = null;
|
|
90
|
+
// вид запроса (msg.re) → время последнего продления (защита от цикла)
|
|
91
|
+
this._renewAttempts = new Map();
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
get hostVote() {
|
|
@@ -172,8 +178,22 @@ export default class GuestSession {
|
|
|
172
178
|
let reconnectAttempt = 0;
|
|
173
179
|
|
|
174
180
|
publisher.on('close', () => {
|
|
175
|
-
const {
|
|
176
|
-
|
|
181
|
+
const {
|
|
182
|
+
baseDelay,
|
|
183
|
+
maxDelay,
|
|
184
|
+
hostFirstDelay = 0,
|
|
185
|
+
} = this._config.reconnect;
|
|
186
|
+
// хост: мастер держит комнату hostDisconnectGraceMs — первая попытка
|
|
187
|
+
// сразу, дальше тот же бэкофф, сдвинутый на одну ступень
|
|
188
|
+
const isHost = Boolean(this._hostRole.controller);
|
|
189
|
+
const delay =
|
|
190
|
+
isHost && reconnectAttempt === 0
|
|
191
|
+
? hostFirstDelay
|
|
192
|
+
: Math.min(
|
|
193
|
+
maxDelay,
|
|
194
|
+
baseDelay *
|
|
195
|
+
2 ** (isHost ? reconnectAttempt - 1 : reconnectAttempt),
|
|
196
|
+
);
|
|
177
197
|
|
|
178
198
|
reconnectAttempt += 1;
|
|
179
199
|
this._timers.setTimeout(() => this._signaling.connect(), delay);
|
|
@@ -309,22 +329,15 @@ export default class GuestSession {
|
|
|
309
329
|
if (action === 'abandonPromotion') {
|
|
310
330
|
this._getPromotion().abandon({ code, report: true });
|
|
311
331
|
} else if (action === 'keepPlaying') {
|
|
312
|
-
// P2P-матчу токен не
|
|
313
|
-
// сообщения — только кодами, тексты в играх)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
{ re: msg.re },
|
|
317
|
-
{ source: 'client' },
|
|
318
|
-
);
|
|
332
|
+
// P2P-матчу токен не нужен, без нового текста в UI (системные
|
|
333
|
+
// сообщения — только кодами, тексты в играх). Членство на мастере
|
|
334
|
+
// вернёт повторный join_room после продления; о неудаче продления
|
|
335
|
+
// пишет _renewAndRetry
|
|
319
336
|
this._membership.stopJoinRetry();
|
|
320
|
-
}
|
|
321
|
-
this._logout();
|
|
337
|
+
}
|
|
322
338
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
'Your session has expired — please sign in again.',
|
|
326
|
-
);
|
|
327
|
-
}
|
|
339
|
+
if (action !== 'abandonPromotion') {
|
|
340
|
+
this._renewAndRetry(msg);
|
|
328
341
|
}
|
|
329
342
|
break;
|
|
330
343
|
}
|
|
@@ -372,6 +385,53 @@ export default class GuestSession {
|
|
|
372
385
|
}
|
|
373
386
|
}
|
|
374
387
|
|
|
388
|
+
// продление входа и повтор отклонённого запроса. Не чаще раза в
|
|
389
|
+
// RENEW_RETRY_MIN_MS на вид запроса: мастер, отвергающий и свежий токен,
|
|
390
|
+
// не должен завести цикл
|
|
391
|
+
async _renewAndRetry(msg) {
|
|
392
|
+
const now = this._now();
|
|
393
|
+
const last = this._renewAttempts.get(msg.re);
|
|
394
|
+
|
|
395
|
+
if (last !== undefined && now - last < RENEW_RETRY_MIN_MS) {
|
|
396
|
+
this._warnTokenExpired(msg);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this._renewAttempts.set(msg.re, now);
|
|
401
|
+
|
|
402
|
+
const result = await this._refreshIdentity();
|
|
403
|
+
|
|
404
|
+
if (result === 'ok') {
|
|
405
|
+
if (msg.re === 'join_room') {
|
|
406
|
+
this._membership.sendJoinRoom();
|
|
407
|
+
} else if (msg.re === 'register_host' || msg.re === 'reclaim_host') {
|
|
408
|
+
this._hostRole.reRegister();
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// хост, не продливший вход, не удержит комнату
|
|
414
|
+
if (
|
|
415
|
+
this._ctx.roomId &&
|
|
416
|
+
(msg.re === 'register_host' || msg.re === 'reclaim_host')
|
|
417
|
+
) {
|
|
418
|
+
this._ui.leaveRoomWith(
|
|
419
|
+
'Your session could not be renewed — please reload the page.',
|
|
420
|
+
);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this._warnTokenExpired(msg);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
_warnTokenExpired(msg) {
|
|
428
|
+
this._diagnostics.warn(
|
|
429
|
+
'engine.session.tokenExpired',
|
|
430
|
+
{ re: msg.re },
|
|
431
|
+
{ source: 'client' },
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
375
435
|
_ownRoom(msg) {
|
|
376
436
|
return Boolean(this._ctx.roomId) && msg.roomId === this._ctx.roomId;
|
|
377
437
|
}
|
package/src/client/style.css
CHANGED
|
@@ -507,12 +507,11 @@ body.hide-cursor {
|
|
|
507
507
|
padding: 5px 0 10px;
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
/* кнопки: провайдеры
|
|
511
|
-
input[type=button], без декоративных стилей, только размер. Create server
|
|
512
|
-
|
|
513
|
-
|
|
510
|
+
/* кнопки: провайдеры Restore, Start, Load more — нативный вид
|
|
511
|
+
input[type=button], без декоративных стилей, только размер. Create server
|
|
512
|
+
сюда не входит: он лежит во флекс-контейнере с gap и центрируется через
|
|
513
|
+
align-self (секция lobby-auth ниже) */
|
|
514
514
|
.lobby-auth-provider,
|
|
515
|
-
#lobby-auth-nick-submit,
|
|
516
515
|
#lobby-more,
|
|
517
516
|
#auth-enter {
|
|
518
517
|
margin: 15px auto;
|
|
@@ -1071,7 +1070,7 @@ body.hide-cursor {
|
|
|
1071
1070
|
display: none;
|
|
1072
1071
|
}
|
|
1073
1072
|
|
|
1074
|
-
/*lobby-auth (
|
|
1073
|
+
/*lobby-auth (экран профиля перед лобби)*/
|
|
1075
1074
|
|
|
1076
1075
|
#lobby-auth {
|
|
1077
1076
|
background-color: #222;
|
|
@@ -1083,33 +1082,137 @@ body.hide-cursor {
|
|
|
1083
1082
|
z-index: 8;
|
|
1084
1083
|
}
|
|
1085
1084
|
|
|
1086
|
-
#lobby-auth-
|
|
1087
|
-
#lobby-auth-
|
|
1085
|
+
#lobby-auth-status,
|
|
1086
|
+
#lobby-auth-welcome {
|
|
1088
1087
|
text-align: center;
|
|
1089
1088
|
}
|
|
1090
1089
|
|
|
1091
|
-
#lobby-auth-
|
|
1090
|
+
#lobby-auth-providers {
|
|
1092
1091
|
display: flex;
|
|
1093
1092
|
flex-direction: column;
|
|
1094
1093
|
gap: 8px;
|
|
1095
1094
|
}
|
|
1096
1095
|
|
|
1097
1096
|
.lobby-auth-provider,
|
|
1098
|
-
#lobby-host
|
|
1099
|
-
#lobby-user-logout {
|
|
1097
|
+
#lobby-host {
|
|
1100
1098
|
align-self: center;
|
|
1101
1099
|
}
|
|
1102
1100
|
|
|
1103
1101
|
#lobby-auth-nick-input {
|
|
1104
1102
|
display: block;
|
|
1105
1103
|
width: 100%;
|
|
1106
|
-
margin
|
|
1104
|
+
margin: 10px 0;
|
|
1105
|
+
box-sizing: border-box;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
#lobby-auth-nick-error,
|
|
1109
|
+
#lobby-auth-restore-error {
|
|
1110
|
+
text-align: left;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
#lobby-auth-register,
|
|
1114
|
+
#lobby-auth-restore,
|
|
1115
|
+
#lobby-auth-status-retry,
|
|
1116
|
+
#lobby-auth-turnstile-retry {
|
|
1117
|
+
margin: 8px auto;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
#lobby-auth-legal,
|
|
1121
|
+
#lobby-auth-lifetime {
|
|
1122
|
+
color: #aaa;
|
|
1123
|
+
font-size: 11px;
|
|
1124
|
+
line-height: 14px;
|
|
1125
|
+
margin: 8px 0;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
#lobby-auth-legal a {
|
|
1129
|
+
color: #ccc;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/* нейтральная плашка уведомления (профиль не найден, выход, удаление) */
|
|
1133
|
+
.form-notice {
|
|
1134
|
+
background-color: #3d3d3d;
|
|
1135
|
+
border: 1px solid #555;
|
|
1136
|
+
padding: 6px 8px;
|
|
1137
|
+
margin: 8px 0;
|
|
1138
|
+
color: #ddd;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.form-notice:empty {
|
|
1142
|
+
display: none;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.form-hint {
|
|
1146
|
+
color: #aaa;
|
|
1147
|
+
font-size: 12px;
|
|
1148
|
+
line-height: 16px;
|
|
1149
|
+
margin: 6px 0;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/*profile*/
|
|
1153
|
+
|
|
1154
|
+
/* панель профиля (profile.pug): открывается вместо #lobby кнопкой бейджа,
|
|
1155
|
+
каркас — как у панели реестра игр (.card, .lobby-column, .games-head) */
|
|
1156
|
+
#profile-panel {
|
|
1157
|
+
background-color: #222;
|
|
1158
|
+
position: absolute;
|
|
1159
|
+
width: 100%;
|
|
1160
|
+
min-height: 100%;
|
|
1161
|
+
top: 0;
|
|
1162
|
+
left: 0;
|
|
1163
|
+
z-index: 8;
|
|
1164
|
+
display: flex;
|
|
1165
|
+
align-items: flex-start;
|
|
1166
|
+
justify-content: center;
|
|
1167
|
+
box-sizing: border-box;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
#profile-panel .card {
|
|
1171
|
+
width: 700px;
|
|
1172
|
+
max-width: 100%;
|
|
1173
|
+
padding: 10px;
|
|
1174
|
+
text-align: left;
|
|
1175
|
+
box-sizing: border-box;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
#profile-link-benefits {
|
|
1179
|
+
margin: 6px 0;
|
|
1180
|
+
padding-left: 20px;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.profile-provider {
|
|
1184
|
+
display: flex;
|
|
1185
|
+
align-items: center;
|
|
1186
|
+
gap: 8px;
|
|
1187
|
+
margin: 6px 0;
|
|
1107
1188
|
}
|
|
1108
1189
|
|
|
1109
|
-
|
|
1110
|
-
#
|
|
1111
|
-
|
|
1190
|
+
/* чекбокс и текст в одну строку, текст переносится под себя */
|
|
1191
|
+
#profile-consent label {
|
|
1192
|
+
display: flex;
|
|
1193
|
+
align-items: flex-start;
|
|
1194
|
+
gap: 8px;
|
|
1112
1195
|
margin: 8px 0;
|
|
1196
|
+
font-size: 12px;
|
|
1197
|
+
line-height: 16px;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
#profile-consent input[type='checkbox'] {
|
|
1201
|
+
flex: none;
|
|
1202
|
+
margin-top: 2px;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
#profile-consent a {
|
|
1206
|
+
color: #ccc;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/* удаление необратимо — блок выделен цветом ошибок */
|
|
1210
|
+
#profile-delete {
|
|
1211
|
+
border: 1px solid #b33;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
#profile-delete .lobby-section-title {
|
|
1215
|
+
color: #e66;
|
|
1113
1216
|
}
|
|
1114
1217
|
|
|
1115
1218
|
/*games*/
|
|
@@ -1320,7 +1423,7 @@ body.hide-cursor {
|
|
|
1320
1423
|
margin-right: 8px;
|
|
1321
1424
|
}
|
|
1322
1425
|
|
|
1323
|
-
/* кнопки входа в
|
|
1426
|
+
/* кнопки входа в панели стоят в бейдже игрока */
|
|
1324
1427
|
.lobby-user-actions {
|
|
1325
1428
|
display: flex;
|
|
1326
1429
|
gap: 8px;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//- скрыт по умолчанию: открывается из main.js
|
|
2
|
-
//-
|
|
1
|
+
//- скрыт по умолчанию: открывается из main.js, когда у вкладки есть личность
|
|
2
|
+
//- (экран профиля #lobby-auth, событие LobbyAuthModel 'authenticated')
|
|
3
3
|
div#lobby(style='display:none')
|
|
4
4
|
//- колоночная обёртка: сам #lobby — флекс-строка, центрирующая содержимое,
|
|
5
5
|
//- и футер под панелями иначе встал бы третьей колонкой рядом с ними
|
|
@@ -8,21 +8,21 @@ div#lobby(style='display:none')
|
|
|
8
8
|
//- левая панель: профиль + форма создания комнаты (lobby-page-plan)
|
|
9
9
|
div#lobby-setup-panel.card
|
|
10
10
|
h2= 'VIMP'
|
|
11
|
-
//- бейдж
|
|
12
|
-
//-
|
|
11
|
+
//- бейдж игрока: управляется LobbyAuthView, показывается вместе с
|
|
12
|
+
//- #lobby, когда у вкладки есть личность
|
|
13
13
|
div#lobby-user
|
|
14
14
|
div
|
|
15
|
-
span= '
|
|
15
|
+
span= 'Playing as '
|
|
16
16
|
b#lobby-user-nick
|
|
17
17
|
//- вход в панель реестра игр (master-game-registry, этап 4):
|
|
18
18
|
//- «My games» видит любой авторизованный, «Moderation» — только
|
|
19
19
|
//- админская роль (main.js по LobbyAuthModel.getRole())
|
|
20
20
|
div.lobby-user-actions
|
|
21
|
+
input#profile-open(type='button', value='Profile')
|
|
21
22
|
input#games-open-mine(type='button', value='My games')
|
|
22
23
|
input#games-open-moderation(type='button', value='Moderation', style='display:none')
|
|
23
24
|
//- журнал клиентских ошибок (plan/client-reports, этап 5): только админ
|
|
24
25
|
input#reports-open(type='button', value='Errors', style='display:none')
|
|
25
|
-
input#lobby-user-logout(type='button', value='Sign out')
|
|
26
26
|
hr.lobby-divider
|
|
27
27
|
h3.lobby-section-title= 'HOST A GAME'
|
|
28
28
|
div#lobby-create
|
|
@@ -69,6 +69,11 @@ div#lobby(style='display:none')
|
|
|
69
69
|
//- футер: ссылку и версию пакета движка проставляет LobbyView
|
|
70
70
|
//- (engineVersion.js + lib/packageLink.js)
|
|
71
71
|
div#lobby-footer
|
|
72
|
+
p
|
|
73
|
+
a#lobby-terms-link(target='_blank', rel='noopener') Terms of Service
|
|
74
|
+
| ·
|
|
75
|
+
|
|
|
76
|
+
a#lobby-privacy-link(target='_blank', rel='noopener') Privacy Policy
|
|
72
77
|
p
|
|
73
78
|
a#lobby-link
|
|
74
79
|
p#lobby-version
|
|
@@ -1,19 +1,37 @@
|
|
|
1
|
-
//-
|
|
2
|
-
//-
|
|
3
|
-
//- фильтруются JS по
|
|
1
|
+
//- экран профиля перед лобби: загрузка, отказ сервиса, Welcome (регистрация
|
|
2
|
+
//- ника и восстановление профиля через OAuth). Кнопки провайдеров
|
|
3
|
+
//- фильтруются JS по списку из /client-config
|
|
4
4
|
div#lobby-auth
|
|
5
5
|
|
|
6
|
-
div#lobby-auth-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
div#lobby-auth-login-buttons
|
|
10
|
-
input.lobby-auth-provider#lobby-auth-login-github(type='button', data-provider='github', value='Sign in with GitHub')
|
|
11
|
-
input.lobby-auth-provider#lobby-auth-login-google(type='button', data-provider='google', value='Sign in with Google')
|
|
12
|
-
input.lobby-auth-provider#lobby-auth-login-apple(type='button', data-provider='apple', value='Sign in with Apple')
|
|
13
|
-
div#lobby-auth-login-error
|
|
6
|
+
div#lobby-auth-status.card(style='display:none')
|
|
7
|
+
p#lobby-auth-status-text
|
|
8
|
+
input#lobby-auth-status-retry(type='button', value='Retry', style='display:none')
|
|
14
9
|
|
|
15
|
-
div#lobby-auth-
|
|
16
|
-
h2= '
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
input#lobby-auth-nick-
|
|
10
|
+
div#lobby-auth-welcome.card(style='display:none')
|
|
11
|
+
h2= 'Welcome to VIMP!'
|
|
12
|
+
p= 'Pick a nick and play. No e-mail or password needed.'
|
|
13
|
+
p#lobby-auth-notice.form-notice
|
|
14
|
+
input#lobby-auth-nick-input.field-text(type='text', maxlength='14', placeholder='Your nick', autocomplete='off')
|
|
15
|
+
div#lobby-auth-nick-error.form-error
|
|
16
|
+
input#lobby-auth-turnstile-retry(type='button', value='Retry', style='display:none')
|
|
17
|
+
//- контейнер виджета Turnstile: виден, только когда проверке нужен клик
|
|
18
|
+
div#lobby-auth-turnstile
|
|
19
|
+
input#lobby-auth-register(type='button', value='Create profile')
|
|
20
|
+
p#lobby-auth-legal
|
|
21
|
+
| By creating a profile you agree to the
|
|
22
|
+
|
|
|
23
|
+
a#lobby-auth-terms(target='_blank', rel='noopener') Terms of Service
|
|
24
|
+
|
|
|
25
|
+
| and the
|
|
26
|
+
|
|
|
27
|
+
a#lobby-auth-privacy(target='_blank', rel='noopener') Privacy Policy
|
|
28
|
+
| .
|
|
29
|
+
p#lobby-auth-lifetime
|
|
30
|
+
hr.lobby-divider
|
|
31
|
+
input#lobby-auth-restore(type='button', value='Restore profile')
|
|
32
|
+
div#lobby-auth-providers(style='display:none')
|
|
33
|
+
p= 'Restore a profile linked to:'
|
|
34
|
+
input.lobby-auth-provider(type='button', data-provider='google', value='Continue with Google')
|
|
35
|
+
input.lobby-auth-provider(type='button', data-provider='discord', value='Continue with Discord')
|
|
36
|
+
input.lobby-auth-provider(type='button', data-provider='github', value='Continue with GitHub')
|
|
37
|
+
div#lobby-auth-restore-error.form-error
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//- Панель профиля в лобби: привязка аккаунта (необязательная), выход,
|
|
2
|
+
//- выгрузка данных, удаление. Скрыта по умолчанию — открывается кнопкой
|
|
3
|
+
//- бейджа пользователя (#profile-open в lobby.pug); id элементов
|
|
4
|
+
//- перечислены в config/lobby.js. Подписи — по-английски, как всё лобби
|
|
5
|
+
div#profile-panel(style='display:none')
|
|
6
|
+
div.lobby-column
|
|
7
|
+
div.games-head
|
|
8
|
+
h3.lobby-section-title= 'Profile'
|
|
9
|
+
input#profile-close(type='button', value='Back to lobby')
|
|
10
|
+
|
|
11
|
+
div.card
|
|
12
|
+
p
|
|
13
|
+
span= 'Nick: '
|
|
14
|
+
b#profile-nick
|
|
15
|
+
p.form-hint= 'The nick cannot be changed.'
|
|
16
|
+
p#profile-status
|
|
17
|
+
|
|
18
|
+
div.card#profile-link
|
|
19
|
+
h3.lobby-section-title= 'Link an account (optional)'
|
|
20
|
+
p= 'Linking is not required — you can keep playing without it. It gives you:'
|
|
21
|
+
ul#profile-link-benefits
|
|
22
|
+
p.form-hint= 'We receive and store only the account ID from the provider — no e-mail, name, avatar or contacts.'
|
|
23
|
+
div#profile-providers
|
|
24
|
+
div#profile-consent
|
|
25
|
+
label
|
|
26
|
+
input#profile-consent-terms(type='checkbox')
|
|
27
|
+
|
|
|
28
|
+
| I have read and agree to the
|
|
29
|
+
|
|
|
30
|
+
a.profile-terms-link(target='_blank', rel='noopener') Terms of Service
|
|
31
|
+
|
|
|
32
|
+
| and the
|
|
33
|
+
|
|
|
34
|
+
a.profile-privacy-link(target='_blank', rel='noopener') Privacy Policy
|
|
35
|
+
| .
|
|
36
|
+
label
|
|
37
|
+
input#profile-consent-data(type='checkbox')
|
|
38
|
+
|
|
|
39
|
+
| I consent to the processing of my personal data — the account ID received from the provider I choose — in order to restore access to my profile, as described in the
|
|
40
|
+
|
|
|
41
|
+
a.profile-privacy-link(target='_blank', rel='noopener') Privacy Policy
|
|
42
|
+
| . I can withdraw this consent at any time by unlinking the account or deleting my profile.
|
|
43
|
+
label
|
|
44
|
+
input#profile-consent-age(type='checkbox')
|
|
45
|
+
|
|
|
46
|
+
| I confirm that I am at least 16 years old, or have reached the age of digital consent in my country if that is higher.
|
|
47
|
+
p#profile-link-notice.form-notice
|
|
48
|
+
div#profile-link-error.form-error
|
|
49
|
+
|
|
50
|
+
div.card#profile-session(style='display:none')
|
|
51
|
+
input#profile-sign-out(type='button', value='Sign out')
|
|
52
|
+
p.form-hint= 'You can sign back in with a linked account.'
|
|
53
|
+
div#profile-session-error.form-error
|
|
54
|
+
|
|
55
|
+
div.card
|
|
56
|
+
input#profile-export(type='button', value='Download my data')
|
|
57
|
+
p.form-hint= 'A JSON file with everything stored about this profile.'
|
|
58
|
+
div#profile-export-error.form-error
|
|
59
|
+
|
|
60
|
+
div.card#profile-delete
|
|
61
|
+
h3.lobby-section-title= 'Delete profile'
|
|
62
|
+
p= 'Deleting the profile is permanent. Your nick, ratings and saved progress in every game will be erased and removed from the leaderboards. The nick will become available to other players.'
|
|
63
|
+
p#profile-delete-admin(style='display:none')= 'Admin profiles cannot be deleted. Remove the nick from the admin list first.'
|
|
64
|
+
input#profile-delete-start(type='button', value='Delete this profile')
|
|
65
|
+
div#profile-delete-confirm(style='display:none')
|
|
66
|
+
p
|
|
67
|
+
span= 'To confirm, type your nick '
|
|
68
|
+
b#profile-delete-nick
|
|
69
|
+
span= ' below:'
|
|
70
|
+
input#profile-delete-input.field-text(type='text', maxlength='15', autocomplete='off')
|
|
71
|
+
input#profile-delete-submit(type='button', value='I understand, delete this profile', disabled)
|
|
72
|
+
input#profile-delete-cancel(type='button', value='Cancel')
|
|
73
|
+
div#profile-delete-error.form-error
|
package/src/config/authClient.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
// Конфиг
|
|
2
|
-
//
|
|
3
|
-
// serviceUrl
|
|
4
|
-
//
|
|
1
|
+
// Конфиг клиента профиля лобби. Бандлится в клиентскую сборку, как
|
|
2
|
+
// config/lobby.js — auth-сервис живёт на отдельном домене/порте, поэтому
|
|
3
|
+
// serviceUrl подставляется под конкретный деплой при сборке (см.
|
|
4
|
+
// docs/en/auth.md).
|
|
5
5
|
export default {
|
|
6
6
|
// базовый URL центрального auth-сервиса (packages/auth). Дефолт — dev-порт
|
|
7
7
|
// из packages/auth/src/config/auth.js; в проде подставляется Vite'ом из
|
|
8
8
|
// VITE_AUTH_SERVICE_URL (см. Dockerfile — тот же VIMP_AUTH_SERVICE_URL,
|
|
9
|
-
// что и серверный master/main.js)
|
|
10
|
-
// больше не требуется. Этот модуль импортируется и серверным
|
|
9
|
+
// что и серверный master/main.js). Этот модуль импортируется и серверным
|
|
11
10
|
// master/main.js под чистым Node, где import.meta.env не определён —
|
|
12
11
|
// typeof-guard нужен, иначе TypeError роняет мастер на старте (502)
|
|
13
12
|
serviceUrl:
|
|
@@ -15,41 +14,66 @@ export default {
|
|
|
15
14
|
import.meta.env.VITE_AUTH_SERVICE_URL) ||
|
|
16
15
|
'http://localhost:3010',
|
|
17
16
|
|
|
18
|
-
//
|
|
19
|
-
|
|
17
|
+
// issuer, которым central auth-сервис подписывает identity-токен; должен
|
|
18
|
+
// совпадать с packages/auth/src/config/auth.js: jwt.issuer — хост сверяет
|
|
19
|
+
// claim iss токена с этим значением при проверке подписи по /jwks
|
|
20
|
+
issuer: 'vimp-auth',
|
|
20
21
|
|
|
21
|
-
// ключ
|
|
22
|
-
|
|
22
|
+
// секрет сессии устройства ('s_…') — единственный ключ к профилю: им
|
|
23
|
+
// получают identity-токен и управляют профилем
|
|
24
|
+
sessionStorageKey: 'vimpSession',
|
|
23
25
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
// сверяет claim iss токена с этим значением при проверке подписи по /jwks
|
|
27
|
-
issuer: 'vimp-auth',
|
|
26
|
+
// dev-логин auth-сервиса (только dev-сборка): ?devSession=s_…
|
|
27
|
+
devSessionParam: 'devSession',
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
error: 'authError',
|
|
29
|
+
// возврат с OAuth-провайдера и незавершённая попытка (sessionStorage)
|
|
30
|
+
oauth: {
|
|
31
|
+
codeParam: 'oauthCode',
|
|
32
|
+
errorParam: 'oauthError',
|
|
33
|
+
pendingKey: 'vimpOauthPending',
|
|
35
34
|
},
|
|
36
35
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
36
|
+
// уведомление, которое должно пережить перезагрузку страницы
|
|
37
|
+
// (sessionStorage)
|
|
38
|
+
noticeKey: 'vimpAuthNotice',
|
|
39
|
+
|
|
40
|
+
// тихое продление identity-токена: за leadMs до exp, повтор при сбое
|
|
41
|
+
// через retryMs
|
|
42
|
+
renew: { leadMs: 3600000, retryMs: 60000 },
|
|
43
|
+
|
|
44
|
+
// Cloudflare Turnstile на регистрации; источник скрипта — тот же, что в
|
|
45
|
+
// CSP мастера (config/master.js security.csp)
|
|
46
|
+
turnstile: {
|
|
47
|
+
scriptUrl:
|
|
48
|
+
'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit',
|
|
49
|
+
// дольше не ждём: скрипт, скорее всего, заблокирован расширением
|
|
50
|
+
loadTimeoutMs: 4000,
|
|
51
|
+
action: 'register',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// DOM-элементы экрана профиля (views/includes/lobbyAuth.pug) и бейджа
|
|
55
|
+
// игрока в лобби (views/includes/lobby.pug)
|
|
39
56
|
elems: {
|
|
40
57
|
containerId: 'lobby-auth',
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
statusId: 'lobby-auth-status',
|
|
59
|
+
statusTextId: 'lobby-auth-status-text',
|
|
60
|
+
statusRetryId: 'lobby-auth-status-retry',
|
|
61
|
+
welcomeId: 'lobby-auth-welcome',
|
|
62
|
+
noticeId: 'lobby-auth-notice',
|
|
44
63
|
nickInputId: 'lobby-auth-nick-input',
|
|
45
64
|
nickErrorId: 'lobby-auth-nick-error',
|
|
46
|
-
|
|
65
|
+
turnstileRetryId: 'lobby-auth-turnstile-retry',
|
|
66
|
+
turnstileId: 'lobby-auth-turnstile',
|
|
67
|
+
registerId: 'lobby-auth-register',
|
|
68
|
+
termsId: 'lobby-auth-terms',
|
|
69
|
+
privacyId: 'lobby-auth-privacy',
|
|
70
|
+
lifetimeId: 'lobby-auth-lifetime',
|
|
71
|
+
restoreId: 'lobby-auth-restore',
|
|
72
|
+
providersId: 'lobby-auth-providers',
|
|
73
|
+
restoreErrorId: 'lobby-auth-restore-error',
|
|
74
|
+
providerClass: 'lobby-auth-provider',
|
|
47
75
|
lobbyId: 'lobby',
|
|
48
76
|
userId: 'lobby-user',
|
|
49
77
|
userNickId: 'lobby-user-nick',
|
|
50
|
-
userLogoutId: 'lobby-user-logout',
|
|
51
78
|
},
|
|
52
|
-
|
|
53
|
-
// CSS-класс кнопок провайдеров в lobbyAuth.pug (data-provider на каждой)
|
|
54
|
-
providerButtonClass: 'lobby-auth-provider',
|
|
55
79
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Английские тексты движковых кодов чата по умолчанию (группы s, v, m, c
|
|
1
|
+
// Английские тексты движковых кодов чата по умолчанию (группы s, v, m, c);
|
|
2
2
|
// индекс = номер кода. Лежат здесь, а не в clientDefaults: при слиянии
|
|
3
3
|
// конфига массивы заменяются целиком, и массив `s` игры стёр бы умолчания.
|
|
4
4
|
// Правило перекрытия: строка игры по тому же индексу важнее умолчания;
|
|
@@ -41,5 +41,4 @@ export default {
|
|
|
41
41
|
],
|
|
42
42
|
m: ['Current map: {0}', 'Next map: {0}'],
|
|
43
43
|
c: ['Command not found', 'Your rank: {0}'],
|
|
44
|
-
n: ['Invalid name', '{0} changed name to {1}'],
|
|
45
44
|
};
|
package/src/config/env.js
CHANGED
|
@@ -20,8 +20,8 @@ export function applyMasterEnv(config, env = process.env) {
|
|
|
20
20
|
config.set('master:port', Number(env.VIMP_MASTER_PORT));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// домен central auth-сервиса
|
|
24
|
-
//
|
|
23
|
+
// домен central auth-сервиса — попадает в CSP connect-src, т.к. лобби
|
|
24
|
+
// делает туда прямые fetch (ручки профиля)
|
|
25
25
|
if (env.VIMP_AUTH_SERVICE_URL) {
|
|
26
26
|
config.set('master:security:authServiceUrl', env.VIMP_AUTH_SERVICE_URL);
|
|
27
27
|
}
|