vimp-engine 0.37.1 → 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 +63 -17
- 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 +45 -0
- 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
|
@@ -4,23 +4,41 @@ import Publisher from '../../../lib/Publisher.js';
|
|
|
4
4
|
|
|
5
5
|
let lobbyAuthView;
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const REGISTER_ERRORS = {
|
|
8
|
+
invalidNick:
|
|
9
|
+
'Invalid nick: 2–14 characters, starting with a letter. Letters, digits, spaces, "_" and "#" are allowed; the last character is a letter, digit or "_".',
|
|
10
|
+
nickTaken: 'This nick is already taken.',
|
|
11
|
+
turnstileBlocked:
|
|
12
|
+
'Security check blocked. Please disable your ad blocker (or Brave Shields) and click Retry.',
|
|
13
|
+
turnstileRequired: 'Security check failed. Please try again.',
|
|
14
|
+
turnstileFailed: 'Security check failed. Please try again.',
|
|
15
|
+
turnstileUnavailable:
|
|
16
|
+
'Registration is temporarily unavailable. Please try again later.',
|
|
17
|
+
rateLimited: 'Too many attempts. Please wait a minute and try again.',
|
|
18
|
+
termsRequired: 'The terms have been updated. Reload the page and try again.',
|
|
12
19
|
};
|
|
13
20
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const REGISTER_ERROR_FALLBACK = 'Network error. Please try again.';
|
|
22
|
+
|
|
23
|
+
const RESTORE_ERRORS = {
|
|
24
|
+
noLinkedProfile:
|
|
25
|
+
'No profile is linked to this account. Create a profile first — you can link the account in Profile settings.',
|
|
26
|
+
denied: 'Sign-in was cancelled.',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const RESTORE_ERROR_FALLBACK = 'Sign-in failed. Please try again.';
|
|
30
|
+
|
|
31
|
+
const NOTICES = {
|
|
32
|
+
profileGone:
|
|
33
|
+
'Your profile was not found — it may have been deleted after a period of inactivity.',
|
|
34
|
+
deleted: 'Your profile has been deleted.',
|
|
35
|
+
signedOut: 'You have signed out.',
|
|
18
36
|
};
|
|
19
37
|
|
|
20
|
-
// Представление
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
38
|
+
// Представление экрана профиля: загрузка, отказ сервиса, Welcome и бейдж
|
|
39
|
+
// игрока в лобби (#lobby-user). Скрывает #lobby целиком, пока у вкладки нет
|
|
40
|
+
// личности; показывает лобби main.js, когда маршрут в него ведёт: вход по
|
|
41
|
+
// ссылке минует лобби вовсе. Текст в DOM — только через textContent.
|
|
24
42
|
export default class LobbyAuthView {
|
|
25
43
|
constructor(model, config) {
|
|
26
44
|
if (lobbyAuthView) {
|
|
@@ -29,69 +47,104 @@ export default class LobbyAuthView {
|
|
|
29
47
|
|
|
30
48
|
lobbyAuthView = this;
|
|
31
49
|
|
|
32
|
-
const { elems
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
const { elems } = config;
|
|
51
|
+
const byId = id => document.getElementById(id);
|
|
52
|
+
|
|
53
|
+
this._model = model;
|
|
54
|
+
this._container = byId(elems.containerId);
|
|
55
|
+
this._status = byId(elems.statusId);
|
|
56
|
+
this._statusText = byId(elems.statusTextId);
|
|
57
|
+
this._statusRetry = byId(elems.statusRetryId);
|
|
58
|
+
this._welcome = byId(elems.welcomeId);
|
|
59
|
+
this._notice = byId(elems.noticeId);
|
|
60
|
+
this._nickInput = byId(elems.nickInputId);
|
|
61
|
+
this._nickError = byId(elems.nickErrorId);
|
|
62
|
+
this._turnstileRetry = byId(elems.turnstileRetryId);
|
|
63
|
+
this._turnstile = byId(elems.turnstileId);
|
|
64
|
+
this._register = byId(elems.registerId);
|
|
65
|
+
this._terms = byId(elems.termsId);
|
|
66
|
+
this._privacy = byId(elems.privacyId);
|
|
67
|
+
this._lifetime = byId(elems.lifetimeId);
|
|
68
|
+
this._restore = byId(elems.restoreId);
|
|
69
|
+
this._providers = byId(elems.providersId);
|
|
70
|
+
this._restoreError = byId(elems.restoreErrorId);
|
|
35
71
|
this._providerButtons = document.querySelectorAll(
|
|
36
|
-
`.${
|
|
72
|
+
`.${elems.providerClass}`,
|
|
37
73
|
);
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
this._loginError = document.getElementById(elems.loginErrorId);
|
|
42
|
-
this._nickSection = document.getElementById(elems.nickSectionId);
|
|
43
|
-
this._nickInput = document.getElementById(elems.nickInputId);
|
|
44
|
-
this._nickError = document.getElementById(elems.nickErrorId);
|
|
45
|
-
this._nickSubmit = document.getElementById(elems.nickSubmitId);
|
|
46
|
-
this._lobby = document.getElementById(elems.lobbyId);
|
|
47
|
-
this._user = document.getElementById(elems.userId);
|
|
48
|
-
this._userNick = document.getElementById(elems.userNickId);
|
|
49
|
-
this._userLogout = document.getElementById(elems.userLogoutId);
|
|
74
|
+
this._lobby = byId(elems.lobbyId);
|
|
75
|
+
this._user = byId(elems.userId);
|
|
76
|
+
this._userNick = byId(elems.userNickId);
|
|
50
77
|
|
|
51
78
|
this.publisher = new Publisher();
|
|
52
79
|
|
|
53
|
-
this.
|
|
54
|
-
btn.onclick = () => this.publisher.emit('login', btn.dataset.provider);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
this._nickSubmit.onclick = () => this._emitNick();
|
|
80
|
+
this._register.onclick = () => this._emitRegister();
|
|
58
81
|
this._nickInput.onkeydown = e => {
|
|
59
82
|
if (e.key === 'Enter') {
|
|
60
|
-
this.
|
|
83
|
+
this._emitRegister();
|
|
61
84
|
}
|
|
62
85
|
};
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
this._restore.onclick = () => {
|
|
87
|
+
this._providers.style.display = 'block';
|
|
88
|
+
};
|
|
89
|
+
this._providerButtons.forEach(btn => {
|
|
90
|
+
btn.onclick = () => this.publisher.emit('restore', btn.dataset.provider);
|
|
91
|
+
});
|
|
92
|
+
this._statusRetry.onclick = () => this.publisher.emit('retry');
|
|
93
|
+
this._turnstileRetry.onclick = () => this.publisher.emit('turnstile-retry');
|
|
65
94
|
|
|
66
95
|
const mp = model.publisher;
|
|
67
96
|
|
|
68
|
-
mp.on('
|
|
69
|
-
mp.on('
|
|
97
|
+
mp.on('loading', 'showLoading', this);
|
|
98
|
+
mp.on('service-error', 'showServiceError', this);
|
|
99
|
+
mp.on('welcome', 'showWelcome', this);
|
|
70
100
|
mp.on('authenticated', 'showLobby', this);
|
|
71
|
-
mp.on('
|
|
72
|
-
mp.on('
|
|
101
|
+
mp.on('register-error', 'renderRegisterError', this);
|
|
102
|
+
mp.on('restore-error', 'renderRestoreError', this);
|
|
73
103
|
}
|
|
74
104
|
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.
|
|
105
|
+
showLoading() {
|
|
106
|
+
this._showStatus('Loading…', false);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
showServiceError() {
|
|
110
|
+
this._showStatus('The profile service is unavailable.', true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_showStatus(text, retry) {
|
|
114
|
+
this._enterAuth();
|
|
115
|
+
this._status.style.display = 'block';
|
|
116
|
+
this._welcome.style.display = 'none';
|
|
117
|
+
this._statusText.textContent = text;
|
|
118
|
+
this._statusRetry.style.display = retry ? '' : 'none';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
showWelcome({ notice = null } = {}) {
|
|
122
|
+
const clientConfig = this._model.getClientConfig();
|
|
123
|
+
const { providers } = clientConfig;
|
|
124
|
+
|
|
125
|
+
this._enterAuth();
|
|
126
|
+
this._status.style.display = 'none';
|
|
127
|
+
this._welcome.style.display = 'block';
|
|
128
|
+
|
|
129
|
+
this._notice.textContent = NOTICES[notice] ?? '';
|
|
130
|
+
this._terms.href = clientConfig.legal.termsUrl;
|
|
131
|
+
this._privacy.href = clientConfig.legal.privacyUrl;
|
|
132
|
+
this._lifetime.textContent =
|
|
133
|
+
'Your profile is kept in this browser and is deleted after ' +
|
|
134
|
+
`${clientConfig.accounts.inactiveDays} days of inactivity. ` +
|
|
135
|
+
'Link an account later to keep it longer.';
|
|
81
136
|
|
|
82
137
|
this._providerButtons.forEach(btn => {
|
|
83
138
|
btn.style.display = providers.includes(btn.dataset.provider)
|
|
84
139
|
? ''
|
|
85
140
|
: 'none';
|
|
86
141
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this._container.style.display = 'block';
|
|
91
|
-
this._loginSection.style.display = 'none';
|
|
92
|
-
this._nickSection.style.display = 'block';
|
|
142
|
+
this._restore.style.display = providers.length > 0 ? '' : 'none';
|
|
143
|
+
this._providers.style.display = 'none';
|
|
144
|
+
this._restoreError.textContent = '';
|
|
93
145
|
this._nickError.textContent = '';
|
|
94
|
-
this.
|
|
146
|
+
this._turnstileRetry.style.display = 'none';
|
|
147
|
+
|
|
95
148
|
this._nickInput.focus();
|
|
96
149
|
}
|
|
97
150
|
|
|
@@ -101,17 +154,52 @@ export default class LobbyAuthView {
|
|
|
101
154
|
this._userNick.textContent = nick;
|
|
102
155
|
}
|
|
103
156
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
157
|
+
// ошибка регистрации по коду; null снимает текст
|
|
158
|
+
renderRegisterError(code) {
|
|
159
|
+
this._nickError.textContent =
|
|
160
|
+
code === null ? '' : (REGISTER_ERRORS[code] ?? REGISTER_ERROR_FALLBACK);
|
|
107
161
|
}
|
|
108
162
|
|
|
109
|
-
|
|
110
|
-
this.
|
|
111
|
-
|
|
163
|
+
renderRestoreError(code) {
|
|
164
|
+
this._providers.style.display = 'block';
|
|
165
|
+
this._restoreError.textContent =
|
|
166
|
+
RESTORE_ERRORS[code] ?? RESTORE_ERROR_FALLBACK;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// блокирует поле и кнопки на время запроса
|
|
170
|
+
setBusy(busy) {
|
|
171
|
+
this._nickInput.disabled = busy;
|
|
172
|
+
this._register.disabled = busy;
|
|
173
|
+
this._restore.disabled = busy;
|
|
174
|
+
this._providerButtons.forEach(btn => {
|
|
175
|
+
btn.disabled = busy;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getTurnstileContainer() {
|
|
180
|
+
return this._turnstile;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
showTurnstileRetry(visible) {
|
|
184
|
+
this._turnstileRetry.style.display = visible ? '' : 'none';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
getNickValue() {
|
|
188
|
+
return this._nickInput.value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
focusNick() {
|
|
192
|
+
this._nickInput.focus();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// экран профиля поверх скрытого лобби
|
|
196
|
+
_enterAuth() {
|
|
197
|
+
this._container.style.display = 'block';
|
|
198
|
+
this._lobby.style.display = 'none';
|
|
199
|
+
this._user.style.display = 'none';
|
|
112
200
|
}
|
|
113
201
|
|
|
114
|
-
|
|
115
|
-
this.publisher.emit('
|
|
202
|
+
_emitRegister() {
|
|
203
|
+
this.publisher.emit('register', this._nickInput.value);
|
|
116
204
|
}
|
|
117
205
|
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
+
|
|
3
|
+
// Singleton ProfileView
|
|
4
|
+
|
|
5
|
+
let profileView;
|
|
6
|
+
|
|
7
|
+
// Тексты ошибок по областям и кодам auth: модель кодов не переводит, вид не
|
|
8
|
+
// ходит в сеть. Язык интерфейса — английский, как у остального лобби
|
|
9
|
+
const GENERIC_ERROR = 'Something went wrong. Please try again.';
|
|
10
|
+
|
|
11
|
+
const LINK_ERRORS = {
|
|
12
|
+
identityInUse:
|
|
13
|
+
'This account is already linked to another profile. To use that profile, delete this one and choose Restore profile on the Welcome screen.',
|
|
14
|
+
providerAlreadyLinked:
|
|
15
|
+
'An account of this provider is already linked to your profile.',
|
|
16
|
+
denied: 'Linking was cancelled.',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DELETE_ERRORS = {
|
|
20
|
+
confirmMismatch: 'The nick does not match.',
|
|
21
|
+
adminAccount: 'Admin profiles cannot be deleted.',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const AREA_ERRORS = {
|
|
25
|
+
link: code => LINK_ERRORS[code] ?? GENERIC_ERROR,
|
|
26
|
+
session: () => 'Could not sign out. Please try again.',
|
|
27
|
+
export: () => 'Could not prepare the file. Please try again.',
|
|
28
|
+
delete: code =>
|
|
29
|
+
DELETE_ERRORS[code] ?? 'Could not delete the profile. Please try again.',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const LINK_SUCCESS =
|
|
33
|
+
'Account linked. You can now restore this profile in any browser.';
|
|
34
|
+
const NO_PROVIDERS = 'Account linking is not available on this server.';
|
|
35
|
+
|
|
36
|
+
// Представление панели профиля: статус, привязка аккаунтов с тремя
|
|
37
|
+
// согласиями, выход, выгрузка данных, удаление с вводом ника. В сеть не
|
|
38
|
+
// ходит и состояния не держит — рисует то, что пришло от модели, и
|
|
39
|
+
// публикует намерения пользователя. Текст в DOM — только через textContent.
|
|
40
|
+
//
|
|
41
|
+
// События: open, close, link <provider>, unlink <provider>, sign-out,
|
|
42
|
+
// export, delete <введённый ник>.
|
|
43
|
+
export default class ProfileView {
|
|
44
|
+
/**
|
|
45
|
+
* @param {Object} model - ProfileModel (источник событий и данных).
|
|
46
|
+
* @param {Object} config - Блок `profile` конфига лобби.
|
|
47
|
+
*/
|
|
48
|
+
constructor(model, config) {
|
|
49
|
+
if (profileView) {
|
|
50
|
+
return profileView;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
profileView = this;
|
|
54
|
+
|
|
55
|
+
const { elems } = config;
|
|
56
|
+
const byId = id => document.getElementById(id);
|
|
57
|
+
|
|
58
|
+
this._model = model;
|
|
59
|
+
this._config = config;
|
|
60
|
+
this._elems = elems;
|
|
61
|
+
|
|
62
|
+
this._panel = byId(elems.panelId);
|
|
63
|
+
this._lobby = byId(elems.lobbyId);
|
|
64
|
+
this._open = byId(elems.openBtnId);
|
|
65
|
+
this._close = byId(elems.closeBtnId);
|
|
66
|
+
this._nick = byId(elems.nickId);
|
|
67
|
+
this._status = byId(elems.statusId);
|
|
68
|
+
this._linkCard = byId(elems.linkCardId);
|
|
69
|
+
this._benefits = byId(elems.benefitsId);
|
|
70
|
+
this._providers = byId(elems.providersId);
|
|
71
|
+
this._consent = byId(elems.consentId);
|
|
72
|
+
this._checks = [
|
|
73
|
+
byId(elems.consentTermsId),
|
|
74
|
+
byId(elems.consentDataId),
|
|
75
|
+
byId(elems.consentAgeId),
|
|
76
|
+
];
|
|
77
|
+
this._linkNotice = byId(elems.linkNoticeId);
|
|
78
|
+
this._linkError = byId(elems.linkErrorId);
|
|
79
|
+
this._sessionCard = byId(elems.sessionCardId);
|
|
80
|
+
this._signOut = byId(elems.signOutBtnId);
|
|
81
|
+
this._exportBtn = byId(elems.exportBtnId);
|
|
82
|
+
this._deleteAdminNote = byId(elems.deleteAdminNoteId);
|
|
83
|
+
this._deleteStart = byId(elems.deleteStartBtnId);
|
|
84
|
+
this._deleteConfirm = byId(elems.deleteConfirmId);
|
|
85
|
+
this._deleteNick = byId(elems.deleteNickId);
|
|
86
|
+
this._deleteInput = byId(elems.deleteInputId);
|
|
87
|
+
this._deleteSubmit = byId(elems.deleteSubmitBtnId);
|
|
88
|
+
this._deleteCancel = byId(elems.deleteCancelBtnId);
|
|
89
|
+
|
|
90
|
+
// строка ошибки каждой области — в своей карточке
|
|
91
|
+
this._errorLines = {
|
|
92
|
+
link: this._linkError,
|
|
93
|
+
session: byId(elems.sessionErrorId),
|
|
94
|
+
export: byId(elems.exportErrorId),
|
|
95
|
+
delete: byId(elems.deleteErrorId),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
this._linkBtns = [];
|
|
99
|
+
this._unlinkBtns = [];
|
|
100
|
+
this._busy = false;
|
|
101
|
+
|
|
102
|
+
this.publisher = new Publisher();
|
|
103
|
+
|
|
104
|
+
this._open.onclick = () => this.publisher.emit('open');
|
|
105
|
+
this._close.onclick = () => this.hide();
|
|
106
|
+
this._signOut.onclick = () => this.publisher.emit('sign-out');
|
|
107
|
+
this._exportBtn.onclick = () => this.publisher.emit('export');
|
|
108
|
+
|
|
109
|
+
this._checks.forEach(check => {
|
|
110
|
+
check.onchange = () => this._syncControls();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this._deleteStart.onclick = () => {
|
|
114
|
+
this._deleteConfirm.style.display = 'block';
|
|
115
|
+
this._deleteInput.focus();
|
|
116
|
+
};
|
|
117
|
+
this._deleteCancel.onclick = () => this._collapseDelete();
|
|
118
|
+
this._deleteInput.oninput = () => this._syncControls();
|
|
119
|
+
this._deleteSubmit.onclick = () =>
|
|
120
|
+
this.publisher.emit('delete', this._deleteInput.value);
|
|
121
|
+
|
|
122
|
+
model.publisher.on('profile', () => this.render());
|
|
123
|
+
model.publisher.on('error', 'renderError', this);
|
|
124
|
+
model.publisher.on('export', 'download', this);
|
|
125
|
+
model.publisher.on('busy', busy => {
|
|
126
|
+
this._busy = busy;
|
|
127
|
+
this._syncControls();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this._renderLegalLinks();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// панель вместо #lobby; профиль рисуется заново, чекбоксы и блок
|
|
134
|
+
// удаления сброшены
|
|
135
|
+
show() {
|
|
136
|
+
this._panel.style.display = 'flex';
|
|
137
|
+
this._lobby.style.display = 'none';
|
|
138
|
+
|
|
139
|
+
this._checks.forEach(check => {
|
|
140
|
+
check.checked = false;
|
|
141
|
+
});
|
|
142
|
+
this._collapseDelete();
|
|
143
|
+
this._clearMessages();
|
|
144
|
+
this.render();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
hide() {
|
|
148
|
+
// закрытой панели закрывать нечего
|
|
149
|
+
if (this._panel.style.display === 'none') {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._panel.style.display = 'none';
|
|
154
|
+
this._lobby.style.display = 'flex';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// true — все три согласия отмечены
|
|
158
|
+
consentGiven() {
|
|
159
|
+
return this._checks.every(check => check.checked);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
render() {
|
|
163
|
+
const profile = this._model.getProfile();
|
|
164
|
+
const clientConfig = this._model.getClientConfig();
|
|
165
|
+
|
|
166
|
+
if (!profile || !clientConfig) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const titles = this._config.providerTitles;
|
|
171
|
+
const title = provider => titles[provider] ?? provider;
|
|
172
|
+
const { inactiveDays, inactiveDaysOAuth } = clientConfig.accounts;
|
|
173
|
+
const linked = new Map(profile.providers.map(p => [p.provider, p]));
|
|
174
|
+
|
|
175
|
+
this._nick.textContent = profile.nick;
|
|
176
|
+
this._deleteNick.textContent = profile.nick;
|
|
177
|
+
|
|
178
|
+
this._renderStatus(profile, title);
|
|
179
|
+
this._renderBenefits(inactiveDays, inactiveDaysOAuth);
|
|
180
|
+
|
|
181
|
+
this._providers.replaceChildren();
|
|
182
|
+
this._linkBtns = [];
|
|
183
|
+
this._unlinkBtns = [];
|
|
184
|
+
|
|
185
|
+
if (clientConfig.providers.length === 0) {
|
|
186
|
+
this._providers.textContent = NO_PROVIDERS;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
clientConfig.providers.forEach(provider => {
|
|
190
|
+
this._providers.append(
|
|
191
|
+
this._renderProviderRow(
|
|
192
|
+
provider,
|
|
193
|
+
title(provider),
|
|
194
|
+
linked.get(provider),
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// согласия нужны, пока есть что привязывать
|
|
200
|
+
this._consent.style.display = clientConfig.providers.some(
|
|
201
|
+
provider => !linked.has(provider),
|
|
202
|
+
)
|
|
203
|
+
? 'block'
|
|
204
|
+
: 'none';
|
|
205
|
+
|
|
206
|
+
this._sessionCard.style.display =
|
|
207
|
+
profile.providers.length > 0 ? 'block' : 'none';
|
|
208
|
+
|
|
209
|
+
const deletable = profile.deletable !== false;
|
|
210
|
+
|
|
211
|
+
this._deleteAdminNote.style.display = deletable ? 'none' : 'block';
|
|
212
|
+
this._deleteStart.style.display = deletable ? '' : 'none';
|
|
213
|
+
|
|
214
|
+
if (!deletable) {
|
|
215
|
+
this._collapseDelete();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this._renderLegalLinks();
|
|
219
|
+
this._syncControls();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_renderStatus(profile, title) {
|
|
223
|
+
const days = profile.inactiveDays;
|
|
224
|
+
|
|
225
|
+
if (profile.role === 'admin') {
|
|
226
|
+
this._status.textContent =
|
|
227
|
+
'Admin profile. It is not deleted for inactivity.';
|
|
228
|
+
} else if (profile.providers.length === 0) {
|
|
229
|
+
this._status.textContent = `Not linked. This profile exists only in this browser and is deleted after ${days} days of inactivity.`;
|
|
230
|
+
} else {
|
|
231
|
+
const names = profile.providers.map(p => title(p.provider)).join(', ');
|
|
232
|
+
|
|
233
|
+
this._status.textContent = `Linked: ${names}. The profile is deleted after ${days} days of inactivity.`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_renderBenefits(baseDays, oauthDays) {
|
|
238
|
+
const items = [
|
|
239
|
+
'Restore your profile in any browser or on any device',
|
|
240
|
+
`The profile is kept for ${oauthDays} days of inactivity instead of ${baseDays}`,
|
|
241
|
+
'Sign out and sign back in',
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
this._benefits.replaceChildren(
|
|
245
|
+
...items.map(text => {
|
|
246
|
+
const li = document.createElement('li');
|
|
247
|
+
|
|
248
|
+
li.textContent = text;
|
|
249
|
+
|
|
250
|
+
return li;
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_renderProviderRow(provider, title, link) {
|
|
256
|
+
const row = document.createElement('div');
|
|
257
|
+
const btn = document.createElement('input');
|
|
258
|
+
|
|
259
|
+
btn.type = 'button';
|
|
260
|
+
row.className = 'profile-provider';
|
|
261
|
+
|
|
262
|
+
if (link) {
|
|
263
|
+
const label = document.createElement('span');
|
|
264
|
+
|
|
265
|
+
label.textContent = `${title} — linked on ${new Date(link.linkedAt).toLocaleDateString()}`;
|
|
266
|
+
btn.value = 'Unlink';
|
|
267
|
+
btn.onclick = () => this.publisher.emit('unlink', provider);
|
|
268
|
+
this._unlinkBtns.push(btn);
|
|
269
|
+
row.append(label, btn);
|
|
270
|
+
} else {
|
|
271
|
+
btn.value = `Link ${title}`;
|
|
272
|
+
btn.onclick = () => this.publisher.emit('link', provider);
|
|
273
|
+
this._linkBtns.push(btn);
|
|
274
|
+
row.append(btn);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return row;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ссылки на условия и политику: в панели и в футере лобби
|
|
281
|
+
_renderLegalLinks() {
|
|
282
|
+
const legal = this._model.getClientConfig()?.legal;
|
|
283
|
+
|
|
284
|
+
if (!legal) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const elems = this._elems;
|
|
289
|
+
const set = (nodes, href) => {
|
|
290
|
+
nodes.forEach(node => {
|
|
291
|
+
if (node) {
|
|
292
|
+
node.href = href;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
set(
|
|
298
|
+
[
|
|
299
|
+
...this._panel.getElementsByClassName(elems.termsLinkClass),
|
|
300
|
+
document.getElementById(elems.footerTermsId),
|
|
301
|
+
],
|
|
302
|
+
legal.termsUrl,
|
|
303
|
+
);
|
|
304
|
+
set(
|
|
305
|
+
[
|
|
306
|
+
...this._panel.getElementsByClassName(elems.privacyLinkClass),
|
|
307
|
+
document.getElementById(elems.footerPrivacyId),
|
|
308
|
+
],
|
|
309
|
+
legal.privacyUrl,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// доступность кнопок от согласий, ввода ника и занятости (запрос идёт)
|
|
314
|
+
_syncControls() {
|
|
315
|
+
const busy = this._busy;
|
|
316
|
+
const nick = this._model.getProfile()?.nick;
|
|
317
|
+
const consent = this.consentGiven();
|
|
318
|
+
|
|
319
|
+
this._linkBtns.forEach(btn => {
|
|
320
|
+
btn.disabled = busy || !consent;
|
|
321
|
+
});
|
|
322
|
+
this._unlinkBtns.forEach(btn => {
|
|
323
|
+
btn.disabled = busy;
|
|
324
|
+
});
|
|
325
|
+
this._signOut.disabled = busy;
|
|
326
|
+
this._exportBtn.disabled = busy;
|
|
327
|
+
this._deleteSubmit.disabled =
|
|
328
|
+
busy || !nick || this._deleteInput.value !== nick;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
_collapseDelete() {
|
|
332
|
+
this._deleteConfirm.style.display = 'none';
|
|
333
|
+
this._deleteInput.value = '';
|
|
334
|
+
this._syncControls();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
_clearMessages() {
|
|
338
|
+
this._linkNotice.textContent = '';
|
|
339
|
+
|
|
340
|
+
Object.values(this._errorLines).forEach(line => {
|
|
341
|
+
line.textContent = '';
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// итог привязки, завершённой при возврате с провайдера
|
|
346
|
+
renderLinkResult({ ok, code }) {
|
|
347
|
+
this._clearMessages();
|
|
348
|
+
|
|
349
|
+
if (ok) {
|
|
350
|
+
this._linkNotice.textContent = LINK_SUCCESS;
|
|
351
|
+
} else {
|
|
352
|
+
this._linkError.textContent = AREA_ERRORS.link(code);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
renderError({ area, code }) {
|
|
357
|
+
this._clearMessages();
|
|
358
|
+
this._errorLines[area].textContent = AREA_ERRORS[area](code);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// выгрузка данных профиля файлом
|
|
362
|
+
download({ data, fileName }) {
|
|
363
|
+
const url = URL.createObjectURL(
|
|
364
|
+
new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }),
|
|
365
|
+
);
|
|
366
|
+
const link = document.createElement('a');
|
|
367
|
+
|
|
368
|
+
link.href = url;
|
|
369
|
+
link.download = fileName;
|
|
370
|
+
link.click();
|
|
371
|
+
URL.revokeObjectURL(url);
|
|
372
|
+
}
|
|
373
|
+
}
|