vimp-engine 0.37.1 → 0.38.1
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/ClientReports.js +2 -1
- package/src/client/components/controller/Games.js +1 -1
- package/src/client/components/controller/LobbyAuth.js +101 -15
- package/src/client/components/controller/Profile.js +95 -0
- package/src/client/components/model/Games.js +1 -1
- package/src/client/components/model/LobbyAuth.js +382 -115
- package/src/client/components/model/Profile.js +144 -0
- package/src/client/components/view/LobbyAuth.js +150 -62
- package/src/client/components/view/Profile.js +372 -0
- package/src/client/lib/authApi.js +106 -0
- package/src/client/lib/hostCaps.js +3 -2
- package/src/client/lib/pkce.js +23 -0
- package/src/client/lib/roomLink.js +1 -1
- package/src/client/lib/sessionSync.js +45 -0
- package/src/client/lib/signalingErrors.js +3 -3
- package/src/client/lib/turnstile.js +130 -0
- package/src/client/main.js +132 -26
- package/src/client/network/TokenRenewer.js +99 -0
- package/src/client/session/GuestSession.js +63 -17
- package/src/client/session/Membership.js +2 -1
- package/src/client/style.css +128 -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 +55 -30
- package/src/config/chatMessages.js +1 -2
- package/src/config/env.js +2 -2
- package/src/config/lobby.js +45 -1
- package/src/config/master.js +8 -5
- 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
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
2
|
import { decodeJwtPayload } from '../../../lib/jwt.js';
|
|
3
|
+
import { isValidNick } from '../../../lib/validators.js';
|
|
4
|
+
import { isSessionRejected } from '../../lib/authApi.js';
|
|
3
5
|
|
|
4
6
|
// Singleton LobbyAuthModel
|
|
5
7
|
|
|
6
8
|
let lobbyAuthModel;
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
const SESSION_RE = /^s_[A-Za-z0-9_-]{43}$/;
|
|
11
|
+
|
|
12
|
+
// Модель профиля лобби: регистрация ника, сессия устройства, identity-токен
|
|
13
|
+
// (в памяти вкладки), возврат с OAuth (Restore и привязка). Сетевые вызовы —
|
|
14
|
+
// через инъектированный authApi, PKCE — через pkce.
|
|
15
|
+
//
|
|
16
|
+
// События: loading, welcome { notice }, authenticated { nick, role, profile },
|
|
17
|
+
// register-error <код>, restore-error <код>, service-error,
|
|
18
|
+
// token-renewed { expiresAt }, session-lost, profile-changed <profile>.
|
|
13
19
|
export default class LobbyAuthModel {
|
|
14
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @param {Object} config - config/authClient.js.
|
|
22
|
+
* @param {Object} deps
|
|
23
|
+
* @param {Object} deps.api - client/lib/authApi.js.
|
|
24
|
+
* @param {Object} deps.pkce - { createVerifier, challengeOf }.
|
|
25
|
+
* @param {Storage} [deps.storage] - секрет сессии.
|
|
26
|
+
* @param {Storage} [deps.sessionStore] - попытка OAuth, уведомление.
|
|
27
|
+
* @param {boolean} [deps.isDev] - принимать ?devSession= (dev-сборка).
|
|
28
|
+
* @param {Function} [deps.getLocation]
|
|
29
|
+
* @param {Function} [deps.now] - () → мс эпохи по часам устройства.
|
|
30
|
+
*/
|
|
31
|
+
constructor(
|
|
32
|
+
config,
|
|
33
|
+
{
|
|
34
|
+
api,
|
|
35
|
+
pkce,
|
|
36
|
+
storage = localStorage,
|
|
37
|
+
sessionStore = sessionStorage,
|
|
38
|
+
isDev = false,
|
|
39
|
+
getLocation = () => window.location,
|
|
40
|
+
now = () => Date.now(),
|
|
41
|
+
} = {},
|
|
42
|
+
) {
|
|
15
43
|
if (lobbyAuthModel) {
|
|
16
44
|
return lobbyAuthModel;
|
|
17
45
|
}
|
|
@@ -19,167 +47,406 @@ export default class LobbyAuthModel {
|
|
|
19
47
|
lobbyAuthModel = this;
|
|
20
48
|
|
|
21
49
|
this._config = config;
|
|
22
|
-
this.
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
50
|
+
this._api = api;
|
|
51
|
+
this._pkce = pkce;
|
|
52
|
+
this._storage = storage;
|
|
53
|
+
this._sessionStore = sessionStore;
|
|
54
|
+
this._isDev = isDev;
|
|
55
|
+
this._getLocation = getLocation;
|
|
56
|
+
this._now = now;
|
|
57
|
+
|
|
58
|
+
this._session = null;
|
|
59
|
+
this._token = null;
|
|
60
|
+
this._tokenExpiresAt = null;
|
|
61
|
+
this._profile = null;
|
|
62
|
+
this._clientConfig = null;
|
|
63
|
+
this._urlParams = { code: null, error: null, devSession: null };
|
|
64
|
+
this._linkResult = null;
|
|
65
|
+
this._refreshing = null;
|
|
25
66
|
|
|
26
67
|
this.publisher = new Publisher();
|
|
27
68
|
}
|
|
28
69
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
const {
|
|
70
|
+
// читает параметры возврата с OAuth и (в dev-сборке) dev-сессию; true —
|
|
71
|
+
// что-то найдено, вызывающий чистит адресную строку. Ищет только в search:
|
|
72
|
+
// колбэк auth собирает адрес через URL.searchParams, поэтому параметры
|
|
73
|
+
// стоят до #, а hash — маршрут #/<gameId>[/<roomId>] (его разбирает RouteBoot)
|
|
74
|
+
takeUrlParams(search) {
|
|
75
|
+
const { oauth, devSessionParam } = this._config;
|
|
35
76
|
const params = new URLSearchParams(search);
|
|
36
77
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
78
|
+
this._urlParams = {
|
|
79
|
+
code: params.get(oauth.codeParam),
|
|
80
|
+
error: params.get(oauth.errorParam),
|
|
81
|
+
devSession: this._isDev ? params.get(devSessionParam) : null,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const { code, error, devSession } = this._urlParams;
|
|
85
|
+
|
|
86
|
+
return Boolean(code || error || devSession);
|
|
87
|
+
}
|
|
40
88
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
|
|
89
|
+
async boot() {
|
|
90
|
+
try {
|
|
91
|
+
await this._boot();
|
|
92
|
+
} catch {
|
|
93
|
+
// иначе экран навсегда остался бы на «Loading…» без Retry
|
|
94
|
+
this.publisher.emit('service-error');
|
|
45
95
|
}
|
|
96
|
+
}
|
|
46
97
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
98
|
+
async _boot() {
|
|
99
|
+
const { sessionStorageKey } = this._config;
|
|
100
|
+
const { devSession } = this._urlParams;
|
|
101
|
+
|
|
102
|
+
if (devSession && SESSION_RE.test(devSession)) {
|
|
103
|
+
this._storage.setItem(sessionStorageKey, devSession);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.publisher.emit('loading');
|
|
107
|
+
|
|
108
|
+
// секрет читается до сети: настройки и токен уходят одновременно, лобби
|
|
109
|
+
// вернувшегося игрока открывается на круг запроса раньше
|
|
110
|
+
const stored = this._storage.getItem(sessionStorageKey);
|
|
111
|
+
// значение не того формата секретом не является: на auth не уходит
|
|
112
|
+
const session = stored && SESSION_RE.test(stored) ? stored : null;
|
|
113
|
+
const [cfg, res] = await Promise.all([
|
|
114
|
+
this._api.clientConfig(),
|
|
115
|
+
session ? this._api.token(session) : null,
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
if (!cfg.ok) {
|
|
119
|
+
this.publisher.emit('service-error');
|
|
120
|
+
return;
|
|
50
121
|
}
|
|
51
122
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
123
|
+
this._clientConfig = cfg.data;
|
|
124
|
+
|
|
125
|
+
// попытка живёт один возврат: любой исход её закрывает
|
|
126
|
+
const attempt = this._takeAttempt();
|
|
127
|
+
const { code, error } = this._urlParams;
|
|
128
|
+
|
|
129
|
+
this._urlParams = { code: null, error: null, devSession: null };
|
|
130
|
+
|
|
131
|
+
if (!session) {
|
|
132
|
+
await this._bootWithoutSession(attempt, code, error);
|
|
133
|
+
return;
|
|
56
134
|
}
|
|
57
135
|
|
|
58
|
-
this.
|
|
59
|
-
|
|
136
|
+
this._session = session;
|
|
137
|
+
|
|
138
|
+
if (res.ok) {
|
|
139
|
+
this._adopt({ token: res.data.token, profile: res.data.profile });
|
|
140
|
+
// уведомление писала вкладка, потерявшая сессию в комнате; под принятой
|
|
141
|
+
// личностью оно ни к чему и не должно всплыть на Welcome позже
|
|
142
|
+
this._takeNotice();
|
|
143
|
+
await this._finishLink(attempt, code, error);
|
|
144
|
+
this._emitAuthenticated();
|
|
145
|
+
} else if (isSessionRejected(res)) {
|
|
146
|
+
this._forgetSession();
|
|
147
|
+
this.publisher.emit('welcome', { notice: 'profileGone' });
|
|
148
|
+
} else {
|
|
149
|
+
// код OAuth одноразовый: Retry доведёт привязку, а не потеряет её
|
|
150
|
+
this._restoreAttempt(attempt, code, error);
|
|
151
|
+
this.publisher.emit('service-error');
|
|
152
|
+
}
|
|
60
153
|
}
|
|
61
154
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
155
|
+
async _bootWithoutSession(attempt, code, error) {
|
|
156
|
+
const notice = this._takeNotice();
|
|
157
|
+
const restoring = attempt?.mode === 'restore';
|
|
158
|
+
|
|
159
|
+
if (restoring && code) {
|
|
160
|
+
const res = await this._api.restore({
|
|
161
|
+
code,
|
|
162
|
+
verifier: attempt.verifier,
|
|
163
|
+
termsVersion: this._clientConfig.legal.version,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (res.ok) {
|
|
167
|
+
this._adopt(res.data);
|
|
168
|
+
this._emitAuthenticated();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.publisher.emit('welcome', { notice });
|
|
173
|
+
this.publisher.emit('restore-error', res.error);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.publisher.emit('welcome', { notice });
|
|
69
178
|
|
|
70
|
-
|
|
179
|
+
if (restoring && error) {
|
|
180
|
+
this.publisher.emit(
|
|
181
|
+
'restore-error',
|
|
182
|
+
error === 'denied' ? 'denied' : 'failed',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
71
185
|
}
|
|
72
186
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
187
|
+
// привязка завершается до authenticated, а не в панели профиля: код живёт
|
|
188
|
+
// 120 с и не должен ждать, пока откроется лобби
|
|
189
|
+
async _finishLink(attempt, code, error) {
|
|
190
|
+
if (attempt?.mode !== 'link') {
|
|
76
191
|
return;
|
|
77
192
|
}
|
|
78
193
|
|
|
79
|
-
|
|
80
|
-
let data;
|
|
194
|
+
const { provider } = attempt;
|
|
81
195
|
|
|
82
|
-
|
|
83
|
-
res = await
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Authorization: `Bearer ${this._pendingToken}`,
|
|
88
|
-
},
|
|
89
|
-
body: JSON.stringify({ nick }),
|
|
196
|
+
if (code) {
|
|
197
|
+
const res = await this._api.linkOauth(this._session, {
|
|
198
|
+
code,
|
|
199
|
+
verifier: attempt.verifier,
|
|
200
|
+
consentVersion: attempt.consentVersion,
|
|
90
201
|
});
|
|
91
|
-
|
|
202
|
+
|
|
203
|
+
if (res.ok) {
|
|
204
|
+
this._profile = res.data.profile;
|
|
205
|
+
this._linkResult = { ok: true, provider };
|
|
206
|
+
} else {
|
|
207
|
+
this._linkResult = { ok: false, provider, code: res.error };
|
|
208
|
+
}
|
|
209
|
+
} else if (error) {
|
|
210
|
+
this._linkResult = {
|
|
211
|
+
ok: false,
|
|
212
|
+
provider,
|
|
213
|
+
code: error === 'denied' ? 'denied' : 'failed',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_restoreAttempt(attempt, code, error) {
|
|
219
|
+
if (attempt) {
|
|
220
|
+
this._sessionStore.setItem(
|
|
221
|
+
this._config.oauth.pendingKey,
|
|
222
|
+
JSON.stringify(attempt),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this._urlParams = { code, error, devSession: null };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_takeAttempt() {
|
|
230
|
+
const key = this._config.oauth.pendingKey;
|
|
231
|
+
const raw = this._sessionStore.getItem(key);
|
|
232
|
+
|
|
233
|
+
this._sessionStore.removeItem(key);
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
return raw ? JSON.parse(raw) : null;
|
|
92
237
|
} catch {
|
|
93
|
-
|
|
94
|
-
return;
|
|
238
|
+
return null;
|
|
95
239
|
}
|
|
240
|
+
}
|
|
96
241
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
242
|
+
_takeNotice() {
|
|
243
|
+
const key = this._config.noticeKey;
|
|
244
|
+
const notice = this._sessionStore.getItem(key);
|
|
245
|
+
|
|
246
|
+
this._sessionStore.removeItem(key);
|
|
247
|
+
|
|
248
|
+
return notice || null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// принять личность: секрет сессии (если пришёл) — в хранилище, токен и
|
|
252
|
+
// профиль — в память
|
|
253
|
+
_adopt({ session, token, profile }) {
|
|
254
|
+
if (session) {
|
|
255
|
+
this._session = session;
|
|
256
|
+
this._storage.setItem(this._config.sessionStorageKey, session);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this._setToken(token);
|
|
260
|
+
this._profile = profile;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
_emitAuthenticated() {
|
|
264
|
+
this.publisher.emit('authenticated', {
|
|
265
|
+
nick: this.getNick(),
|
|
266
|
+
role: this.getRole(),
|
|
267
|
+
profile: this._profile,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_forgetSession() {
|
|
272
|
+
const key = this._config.sessionStorageKey;
|
|
273
|
+
|
|
274
|
+
// в хранилище может лежать секрет ДРУГОГО профиля: его записала вкладка,
|
|
275
|
+
// сменившая личность браузера, пока эта доигрывала матч под прежней.
|
|
276
|
+
// Стирается только свой — у профиля без привязки он единственный ключ
|
|
277
|
+
if (this._storage.getItem(key) === this._session) {
|
|
278
|
+
this._storage.removeItem(key);
|
|
100
279
|
}
|
|
101
280
|
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
281
|
+
this._session = null;
|
|
282
|
+
this._token = null;
|
|
283
|
+
this._tokenExpiresAt = null;
|
|
284
|
+
this._profile = null;
|
|
104
285
|
}
|
|
105
286
|
|
|
106
|
-
|
|
107
|
-
|
|
287
|
+
validateNick(nick) {
|
|
288
|
+
return isValidNick(nick);
|
|
289
|
+
}
|
|
108
290
|
|
|
109
|
-
|
|
110
|
-
|
|
291
|
+
async register(nick, turnstileToken) {
|
|
292
|
+
const res = await this._api.register({
|
|
293
|
+
nick,
|
|
294
|
+
turnstileToken,
|
|
295
|
+
termsVersion: this._clientConfig.legal.version,
|
|
296
|
+
});
|
|
111
297
|
|
|
112
|
-
|
|
298
|
+
if (!res.ok) {
|
|
299
|
+
this.publisher.emit('register-error', res.error);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this._adopt(res.data);
|
|
304
|
+
this._emitAuthenticated();
|
|
305
|
+
|
|
306
|
+
return true;
|
|
113
307
|
}
|
|
114
308
|
|
|
115
|
-
|
|
116
|
-
|
|
309
|
+
// URL старта OAuth для Restore; попытка (verifier) остаётся в
|
|
310
|
+
// sessionStorage вкладки, на auth уходит только challenge
|
|
311
|
+
async restoreUrl(provider) {
|
|
312
|
+
return this._oauthUrl('restore', provider);
|
|
117
313
|
}
|
|
118
314
|
|
|
119
|
-
|
|
120
|
-
|
|
315
|
+
// то же для привязки из панели профиля; consentVersion — версия
|
|
316
|
+
// согласий, принятых чекбоксами перед переходом
|
|
317
|
+
async linkUrl(provider, consentVersion) {
|
|
318
|
+
return this._oauthUrl('link', provider, { consentVersion });
|
|
121
319
|
}
|
|
122
320
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
321
|
+
async _oauthUrl(mode, provider, extra = {}) {
|
|
322
|
+
const verifier = this._pkce.createVerifier();
|
|
323
|
+
const challenge = await this._pkce.challengeOf(verifier);
|
|
324
|
+
const { origin, pathname, hash } = this._getLocation();
|
|
325
|
+
|
|
326
|
+
this._sessionStore.setItem(
|
|
327
|
+
this._config.oauth.pendingKey,
|
|
328
|
+
JSON.stringify({ mode, provider, verifier, ...extra }),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// hash (#/<gameId>[/<roomId>]) едет в returnUrl: после возврата игрок
|
|
332
|
+
// попадает туда, куда вёл открытый адрес
|
|
333
|
+
return this._api.oauthStartUrl(provider, {
|
|
334
|
+
mode,
|
|
335
|
+
returnUrl: `${origin}${pathname}${hash}`,
|
|
336
|
+
challenge,
|
|
337
|
+
});
|
|
129
338
|
}
|
|
130
339
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
340
|
+
// продление identity-токена по сессии; параллельные вызовы делят запрос
|
|
341
|
+
refresh() {
|
|
342
|
+
if (!this._refreshing) {
|
|
343
|
+
this._refreshing = this._refresh().finally(() => {
|
|
344
|
+
this._refreshing = null;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
136
347
|
|
|
137
|
-
return
|
|
348
|
+
return this._refreshing;
|
|
138
349
|
}
|
|
139
350
|
|
|
140
|
-
|
|
141
|
-
|
|
351
|
+
async _refresh() {
|
|
352
|
+
if (!this._session) {
|
|
353
|
+
return 'lost';
|
|
354
|
+
}
|
|
142
355
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.
|
|
356
|
+
const res = await this._api.token(this._session);
|
|
357
|
+
|
|
358
|
+
if (res.ok) {
|
|
359
|
+
this._setToken(res.data.token);
|
|
360
|
+
this._profile = res.data.profile;
|
|
361
|
+
this.publisher.emit('token-renewed', {
|
|
362
|
+
expiresAt: this.getTokenExpiresAt(),
|
|
363
|
+
});
|
|
364
|
+
return 'ok';
|
|
147
365
|
}
|
|
366
|
+
|
|
367
|
+
if (isSessionRejected(res)) {
|
|
368
|
+
this._forgetSession();
|
|
369
|
+
this.publisher.emit('session-lost');
|
|
370
|
+
return 'lost';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return 'retry';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// итог привязки, завершённой в boot(); отдаётся один раз
|
|
377
|
+
takeLinkResult() {
|
|
378
|
+
const result = this._linkResult;
|
|
379
|
+
|
|
380
|
+
this._linkResult = null;
|
|
381
|
+
|
|
382
|
+
return result;
|
|
148
383
|
}
|
|
149
384
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
385
|
+
setProfile(profile) {
|
|
386
|
+
this._profile = profile;
|
|
387
|
+
this.publisher.emit('profile-changed', profile);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// выход и удаление профиля (панель профиля)
|
|
391
|
+
clearSession() {
|
|
392
|
+
this._forgetSession();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// уведомление для экрана Welcome после перезагрузки страницы
|
|
396
|
+
setNotice(code) {
|
|
397
|
+
this._sessionStore.setItem(this._config.noticeKey, code);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
getToken() {
|
|
401
|
+
return this._token;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getNick() {
|
|
405
|
+
return this._profile?.nick ?? null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// роль из payload токена — ПОДСКАЗКА ИНТЕРФЕЙСУ (показывать ли кнопку
|
|
409
|
+
// модерации), не право: права админа auth-сервис проверяет сам, по нику
|
|
410
|
+
// из VIMP_ADMIN_NICKS
|
|
411
|
+
getRole() {
|
|
412
|
+
return decodeJwtPayload(this._token)?.role ?? 'user';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Срок токена в шкале часов УСТРОЙСТВА: момент получения плюс время жизни
|
|
416
|
+
// из самого токена (exp − iat — оба выставлены часами auth-сервиса).
|
|
417
|
+
// Сдвиг часов устройства на срок не влияет. Токен без iat — срок по exp
|
|
418
|
+
_setToken(token) {
|
|
155
419
|
const payload = decodeJwtPayload(token);
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!payload || !payload.nick || isExpired) {
|
|
160
|
-
delete localStorage[tokenStorageKey];
|
|
161
|
-
|
|
162
|
-
// Тихое восстановление из localStorage (persist === false): просроченный/
|
|
163
|
-
// битый токен — штатная ситуация возврата на сайт, не ошибка. login-error
|
|
164
|
-
// — только на интерактивном пути.
|
|
165
|
-
if (persist) {
|
|
166
|
-
this.publisher.emit(
|
|
167
|
-
'login-error',
|
|
168
|
-
isExpired ? 'tokenExpired' : 'invalidToken',
|
|
169
|
-
);
|
|
170
|
-
}
|
|
420
|
+
const exp = payload?.exp;
|
|
421
|
+
const iat = payload?.iat;
|
|
171
422
|
|
|
172
|
-
|
|
173
|
-
|
|
423
|
+
this._token = token;
|
|
424
|
+
|
|
425
|
+
if (typeof exp !== 'number') {
|
|
426
|
+
this._tokenExpiresAt = null;
|
|
427
|
+
} else if (typeof iat === 'number' && exp > iat) {
|
|
428
|
+
this._tokenExpiresAt = this._now() + (exp - iat) * 1000;
|
|
429
|
+
} else {
|
|
430
|
+
this._tokenExpiresAt = exp * 1000;
|
|
174
431
|
}
|
|
432
|
+
}
|
|
175
433
|
|
|
176
|
-
|
|
177
|
-
|
|
434
|
+
// срок входа (мс эпохи по часам устройства) или null — неизвестен.
|
|
435
|
+
// Вкладка с кончающимся токеном не принимает роль хоста:
|
|
436
|
+
// register_host/reclaim_host предъявят его мастеру посреди матча
|
|
437
|
+
getTokenExpiresAt() {
|
|
438
|
+
return this._tokenExpiresAt;
|
|
439
|
+
}
|
|
178
440
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
441
|
+
getProfile() {
|
|
442
|
+
return this._profile;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
getClientConfig() {
|
|
446
|
+
return this._clientConfig;
|
|
447
|
+
}
|
|
182
448
|
|
|
183
|
-
|
|
449
|
+
getSession() {
|
|
450
|
+
return this._session;
|
|
184
451
|
}
|
|
185
452
|
}
|