Blame Raw
· · · 291 lines (8.1 KB)
0 contributors
1import { initAccessibility } from './a11y.js';
2
3/**
4 * CRSF token for requests.
5 */
6let csrfToken = '';
7let discreetLogin = false;
8
9/**
10 * Gets a CSRF token from the server.
11 * @returns {Promise<string>} CSRF token
12 */
13async function getCsrfToken() {
14 const response = await fetch('/csrf-token');
15 const data = await response.json();
16 return data.token;
17}
18
19/**
20 * Gets a list of users from the server.
21 * @returns {Promise<object>} List of users
22 */
23async function getUserList() {
24 const response = await fetch('/api/users/list', {
25 method: 'POST',
26 headers: {
27 'Content-Type': 'application/json',
28 'X-CSRF-Token': csrfToken,
29 },
30 });
31
32 if (!response.ok) {
33 const errorData = await response.json();
34 return displayError(errorData.error || 'An error occurred');
35 }
36
37 if (response.status === 204) {
38 discreetLogin = true;
39 return [];
40 }
41
42 const userListObj = await response.json();
43 console.log(userListObj);
44 return userListObj;
45}
46
47/**
48 * Requests a recovery code for the user.
49 * @param {string} handle User handle
50 * @returns {Promise<void>}
51 */
52async function sendRecoveryPart1(handle) {
53 const response = await fetch('/api/users/recover-step1', {
54 method: 'POST',
55 headers: {
56 'Content-Type': 'application/json',
57 'X-CSRF-Token': csrfToken,
58 },
59 body: JSON.stringify({ handle }),
60 });
61
62 if (!response.ok) {
63 const errorData = await response.json();
64 return displayError(errorData.error || 'An error occurred');
65 }
66
67 showRecoveryBlock();
68}
69
70/**
71 * Sets a new password for the user using the recovery code.
72 * @param {string} handle User handle
73 * @param {string} code Recovery code
74 * @param {string} newPassword New password
75 * @returns {Promise<void>}
76 */
77async function sendRecoveryPart2(handle, code, newPassword) {
78 const recoveryData = {
79 handle,
80 code,
81 newPassword,
82 };
83
84 const response = await fetch('/api/users/recover-step2', {
85 method: 'POST',
86 headers: {
87 'Content-Type': 'application/json',
88 'X-CSRF-Token': csrfToken,
89 },
90 body: JSON.stringify(recoveryData),
91 });
92
93 if (!response.ok) {
94 const errorData = await response.json();
95 return displayError(errorData.error || 'An error occurred');
96 }
97
98 console.log(`Successfully recovered password for ${handle}!`);
99 await performLogin(handle, newPassword);
100}
101
102/**
103 * Attempts to log in the user.
104 * @param {string} handle User's handle
105 * @param {string} password User's password
106 * @returns {Promise<void>}
107 */
108async function performLogin(handle, password) {
109 const userInfo = {
110 handle: handle,
111 password: password,
112 };
113
114 try {
115 const response = await fetch('/api/users/login', {
116 method: 'POST',
117 headers: {
118 'Content-Type': 'application/json',
119 'X-CSRF-Token': csrfToken,
120 },
121 body: JSON.stringify(userInfo),
122 });
123
124 if (!response.ok) {
125 const errorData = await response.json();
126 return displayError(errorData.error || 'An error occurred');
127 }
128
129 const data = await response.json();
130
131 if (data.handle) {
132 console.log(`Successfully logged in as ${handle}!`);
133 redirectToHome();
134 }
135 } catch (error) {
136 console.error('Error logging in:', error);
137 displayError(String(error));
138 }
139}
140
141/**
142 * Handles the user selection event.
143 * @param {object} user User object
144 * @returns {Promise<void>}
145 */
146async function onUserSelected(user) {
147 // No password, just log in
148 if (!user.password) {
149 return await performLogin(user.handle, '');
150 }
151
152 $('#passwordRecoveryBlock').hide();
153 $('#passwordEntryBlock').show();
154 $('#loginButton').off('click').on('click', async () => {
155 const password = String($('#userPassword').val());
156 await performLogin(user.handle, password);
157 });
158
159 $('#recoverPassword').off('click').on('click', async () => {
160 await sendRecoveryPart1(user.handle);
161 });
162
163 $('#sendRecovery').off('click').on('click', async () => {
164 const code = String($('#recoveryCode').val());
165 const newPassword = String($('#newPassword').val());
166 await sendRecoveryPart2(user.handle, code, newPassword);
167 });
168
169 displayError('');
170}
171
172/**
173 * Displays an error message to the user.
174 * @param {string} message Error message
175 */
176function displayError(message) {
177 $('#errorMessage').text(message);
178}
179
180/**
181 * Redirects the user to the home page.
182 * Preserves the query string.
183 */
184function redirectToHome() {
185 // Create a URL object based on the current location
186 const currentUrl = new URL(window.location.href);
187
188 // After a login there's no need to preserve the
189 // noauto parameter (if present)
190 currentUrl.searchParams.delete('noauto');
191
192 // Set the pathname to root and keep the updated query string
193 currentUrl.pathname = '/';
194
195 // Redirect to the new URL
196 window.location.href = currentUrl.toString();
197}
198
199/**
200 * Hides the password entry block and shows the password recovery block.
201 */
202function showRecoveryBlock() {
203 $('#passwordEntryBlock').hide();
204 $('#passwordRecoveryBlock').show();
205 displayError('');
206}
207
208/**
209 * Hides the password recovery block and shows the password entry block.
210 */
211function onCancelRecoveryClick() {
212 $('#passwordRecoveryBlock').hide();
213 $('#passwordEntryBlock').show();
214 displayError('');
215}
216
217/**
218 * Configures the login page for normal login.
219 * @param {import('../../src/users').UserViewModel[]} userList List of users
220 */
221function configureNormalLogin(userList) {
222 console.log('Discreet login is disabled');
223 $('#handleEntryBlock').hide();
224 $('#normalLoginPrompt').show();
225 $('#discreetLoginPrompt').hide();
226 console.log(userList);
227 for (const user of userList) {
228 const userBlock = $('<div></div>').addClass('userSelect');
229 const avatarBlock = $('<div></div>').addClass('avatar');
230 avatarBlock.append($('<img>').attr('src', user.avatar));
231 userBlock.append(avatarBlock);
232 userBlock.append($('<span></span>').addClass('userName').text(user.name));
233 userBlock.append($('<small></small>').addClass('userHandle').text(user.handle));
234 userBlock.on('click', () => onUserSelected(user));
235 $('#userList').append(userBlock);
236 }
237}
238
239/**
240 * Configures the login page for discreet login.
241 */
242function configureDiscreetLogin() {
243 console.log('Discreet login is enabled');
244 $('#handleEntryBlock').show();
245 $('#normalLoginPrompt').hide();
246 $('#discreetLoginPrompt').show();
247 $('#userList').hide();
248 $('#passwordRecoveryBlock').hide();
249 $('#passwordEntryBlock').show();
250 $('#loginButton').off('click').on('click', async () => {
251 const handle = String($('#userHandle').val());
252 const password = String($('#userPassword').val());
253 await performLogin(handle, password);
254 });
255
256 $('#recoverPassword').off('click').on('click', async () => {
257 const handle = String($('#userHandle').val());
258 await sendRecoveryPart1(handle);
259 });
260
261 $('#sendRecovery').off('click').on('click', async () => {
262 const handle = String($('#userHandle').val());
263 const code = String($('#recoveryCode').val());
264 const newPassword = String($('#newPassword').val());
265 await sendRecoveryPart2(handle, code, newPassword);
266 });
267}
268
269(async function () {
270 initAccessibility();
271
272 csrfToken = await getCsrfToken();
273 const userList = await getUserList();
274
275 if (discreetLogin) {
276 configureDiscreetLogin();
277 } else {
278 configureNormalLogin(userList);
279 }
280 document.getElementById('shadow_popup').style.opacity = '';
281 $('#cancelRecovery').on('click', onCancelRecoveryClick);
282 $(document).on('keydown', (evt) => {
283 if (evt.key === 'Enter' && document.activeElement.tagName === 'INPUT') {
284 if ($('#passwordRecoveryBlock').is(':visible')) {
285 $('#sendRecovery').trigger('click');
286 } else {
287 $('#loginButton').trigger('click');
288 }
289 }
290 });
291})();