Files
puter/src/gui/src/UI/UIWindowSignup.js
T
Juan Castro 29c6f78b7c fix: refuse opaque origins cleanly instead of a logged 400
A page the browser gives no origin to — one opened straight from disk, or an
iframe sandboxed without `allow-same-origin` — serialises its origin as
`"null"`. `appUidFromOrigin` logged that at error level on every attempt and
the GUI sent it on three endpoints, so each refusal cost a 400 and an
error-level line while the user got nothing useful.

An opaque origin cannot name an app to mint a token for, and is not a valid
`postMessage` target to deliver one to, so these clients are refused rather
than supported. Make the refusal quiet, early and legible instead:

- AuthService: drop the `console.error` and say what the caller should do.
  The accept/reject set is unchanged; a 400 is already counted per route.
- puter.js: `signIn()` rejects with `unsupported_origin` before opening
  anything, which covers implicit auth too since it routes through `signIn`.
  The load-time warning now covers sandboxed iframes, console-only — a modal
  belongs in nobody else's embed.
- GUI: a popup whose opener has no attested origin says so and closes,
  instead of rendering a blank window and wedging on a failed exchange.

Also fixes two faults this made reachable: `PuterDialog.open()` prefers the
popup branch, which reaches the `puter` global before the constructor that
assigns it has returned (`puter is not defined`, no SDK at all); and
`showModal()` throws where modals are blocked. `openNotice()` does neither.

Ordinary http(s) sign-in is unaffected.
2026-10-01 11:09:04 -04:00

815 lines
43 KiB
JavaScript

/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import check_password_strength from '../helpers/checkPasswordStrength.js';
import UIWindow from './UIWindow.js';
import UIWindowEmailConfirmationRequired from './UIWindowEmailConfirmationRequired.js';
import UIWindowPhoneVerificationRequired from './UIWindowPhoneVerificationRequired.js';
import UIWindowCardVerificationRequired from './UIWindowCardVerificationRequired.js';
import UIWindowLogin from './UIWindowLogin.js';
import { KNOWN_OIDC_PROVIDERS, OIDC_GENERIC_PROVIDER_ICON, humanizeOidcProviderId } from '../util/openid.js';
import { offersFederatedSignInInPopup } from '../util/popupAuth.js';
import { get_auth_redirect_url, get_oidc_return_to } from '../helpers/authRedirect.js';
import { authLogoHeader, wireAuthLogoHeader } from '../helpers/authLogoHeader.js';
import { isAttestedOrigin } from '../helpers/attestedOrigin.js';
import { bonusRequirementsKey, checkSignupBonusCode } from '../helpers/signupBonusCode.js';
// What a checked bonus code grants, above the form (and the provider buttons).
function renderBonusNotice(el_window, result) {
const $notice = $(el_window).find('.signup-bonus-notice');
if (!result) {
$notice.hide().empty();
return;
}
if (!result.valid) {
$notice
.addClass('signup-bonus-notice-invalid')
.html(`<p>${i18n('signup_bonus_code_invalid_retry')}</p>`)
.show();
return;
}
const requirementsKey = bonusRequirementsKey(result.requirements);
let h = `<strong>${html_encode(result.display.title)}</strong>`;
h += `<p>${html_encode(result.display.description)}</p>`;
if (requirementsKey) {
h += `<p class="signup-bonus-requirements">${i18n(requirementsKey)}</p>`;
}
$notice.removeClass('signup-bonus-notice-invalid').html(h).show();
}
function UIWindowSignup(options) {
options = options ?? {};
options.reload_on_success = options.reload_on_success ?? true;
options.has_head = options.has_head ?? true;
options.send_confirmation_code = options.send_confirmation_code ?? false;
options.show_close_button = options.show_close_button ?? true;
if (options.redirect_url === undefined) {
// stay on the page the signup started from (e.g. an /app/<name>
// landing), with standalone auth pages going to the root dashboard
options.redirect_url = get_auth_redirect_url();
}
return new Promise(async (resolve) => {
const internal_id = window.uuidv4();
const logo_clickable =
!!options.window_options?.cover_page && !window.embedded_in_popup;
let h = '';
h +=
'<div style="margin: 0 auto;">';
// logo
h += authLogoHeader({
logoSrc: window.icons['logo-white.svg'],
logoClickable: logo_clickable,
openerOrigin: window.embedded_in_popup && isAttestedOrigin(window.openerOrigin) ? window.openerOrigin : '',
openerFallbackSrc: window.icons['website.svg'],
});
// close button
if (!options.has_head && options.show_close_button !== false) {
h += '<div class="generic-close-window-button"> &times; </div>';
}
// Form
h += '<div style="padding: 15px;">';
// title
h += `<h1 class="signup-form-title">${i18n('create_free_account')}</h1>`;
// In a sign-in popup, say which site brought the user here.
if (window.embedded_in_popup && isAttestedOrigin(window.openerOrigin)) {
h += `<p class="auth-opener-notice">${i18n('popup_opener_uses_puter', [new URL(window.openerOrigin).hostname])}</p>`;
}
h += '<div class="signup-bonus-notice" style="display:none;"></div>';
// signup form
h += '<form class="signup-form">';
// error msg
h += '<div class="signup-error-msg"></div>';
// username
h += '<div style="overflow: hidden;">';
h += `<label for="username-${internal_id}">${i18n('username')}</label>`;
h += `<input id="username-${internal_id}" value="${html_encode(options.username ?? '')}" class="username" type="text" autocomplete="username" spellcheck="false" autocorrect="off" autocapitalize="off" data-gramm_editor="false"/>`;
h += '</div>';
// email
h += '<div style="overflow: hidden; margin-top: 10px;">';
h += `<label for="email-${internal_id}">${i18n('email')}</label>`;
h += `<input id="email-${internal_id}" value="${html_encode(options.email ?? '')}" class="email" type="email" autocomplete="email" spellcheck="false" autocorrect="off" autocapitalize="off" data-gramm_editor="false"/>`;
h += '</div>';
// password
h +=
'<div style="overflow: hidden; margin-top: 10px; position: relative;">';
h += `<label for="password-${internal_id}">${i18n('password')}</label>`;
h += `<input id="password-${internal_id}" class="password" type="${options.show_password ? 'text' : 'password'}" name="password" autocomplete="new-password" />`;
// show/hide icon
h += `<span style="position: absolute; right: 5%; top: 50%; cursor: pointer;" id="toggle-show-password-${internal_id}">
<img class="toggle-show-password-icon" src="${options.show_password ? window.icons['eye-closed.svg'] : window.icons['eye-open.svg']}" width="20" height="20">
</span>`;
h += '</div>';
// confirm password
h +=
'<div style="overflow: hidden; margin-top: 10px; margin-bottom: 10px; position: relative">';
h += `<label for="confirm-password-${internal_id}">${i18n('signup_confirm_password')}</label>`;
h += `<input id="confirm-password-${internal_id}" class="confirm-password" type="${options.show_password ? 'text' : 'password'}" name="confirm-password" autocomplete="new-password" />`;
// show/hide icon
h += `<span style="position: absolute; right: 5%; top: 50%; cursor: pointer;" id="toggle-show-password-${internal_id}">
<img class="toggle-show-password-icon" src="${options.show_password ? window.icons['eye-closed.svg'] : window.icons['eye-open.svg']}" width="20" height="20">
</span>`;
h += '</div>';
// bot trap - if this value is submitted server will ignore the request
h +=
'<input type="text" name="p102xyzname" class="p102xyzname" value="">';
// Turnstile widget (only when enabled)
if (window.gui_params?.turnstileSiteKey) {
h +=
'<div style="min-height: 20px; display: flex; justify-content: center;">';
// appearance: always/execute/interaction-only
// docs: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/widget-configurations/?utm_source=chatgpt.com#appearance-modes
h += `<div class="cf-turnstile" data-sitekey="${window.gui_params.turnstileSiteKey}" data-appearance="interaction-only"></div>`;
h += '</div>';
}
// Create Account
h += `<button class="signup-btn button button-primary button-block button-normal">${i18n('create_free_account')}</button>`;
h += '</form>';
h +=
'<div class="oidc-providers-wrapper" style="display:none; padding: 10px 0;">';
h += `<button type="button" class="oidc-google-btn oidc-btn button button-block button-normal" style="display:none; align-items:center; justify-content:center; gap:8px;"><img style="width: 20px; height: 20px;" src="data:image/webp;base64,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" />${i18n('sign_up_with_google')}</button>`;
h += `<button type="button" class="oidc-microsoft-btn oidc-btn button button-block button-normal" style="display:none; align-items:center; justify-content:center; gap:8px; margin-top:8px;"><svg style="width:20px; height:20px;" viewBox="0 0 23 23" fill="none"><rect x="1" y="1" width="10" height="10" fill="#f25022"/><rect x="12" y="1" width="10" height="10" fill="#7fba00"/><rect x="1" y="12" width="10" height="10" fill="#00a4ef"/><rect x="12" y="12" width="10" height="10" fill="#ffb900"/></svg>${i18n('sign_up_with_microsoft')}</button>`;
h += `<button type="button" class="oidc-apple-btn oidc-btn button button-block button-normal" style="display:none; align-items:center; justify-content:center; gap:8px; margin-top:8px;"><svg style="width:20px; height:20px;" viewBox="0 0 384 512" fill="currentColor"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg>${i18n('sign_up_with_apple')}</button>`;
h += '<div class="oidc-custom-providers"></div>';
h += '</div>';
// "Sign up with email" button, shown only when OIDC providers are available
h += `<div class="signup-with-email-wrapper" style="display:none;">`;
h += `<button class="signup-with-email-btn oidc-btn button button-block button-normal">${i18n('sign_up_with_email')}</button>`;
h += `</div>`;
// terms and privacy
h += `<p class="signup-terms">${i18n('tos_fineprint', [], false)}</p>`;
h += '</div>';
// login link
// create account link
h += '<div class="c2a-wrapper" style="padding:15px;">';
h += `<button class="login-c2a-clickable">${i18n('log_in')}</button>`;
h += '</div>';
h += '</div>';
const el_window = await UIWindow({
title: null,
app: 'signup',
single_instance: true,
icon: null,
uid: null,
is_dir: false,
body_content: h,
draggable_body: false,
has_head: true,
selectable_body: false,
allow_context_menu: false,
is_draggable: true,
is_droppable: false,
is_resizable: false,
stay_on_top: false,
allow_native_ctxmenu: true,
allow_user_select: true,
...options.window_options,
dominant: true,
center: true,
onAppend: function (el_window) {
if (options.authError) {
$(el_window)
.find('.signup-error-msg')
.html(html_encode(options.authError))
.fadeIn();
}
if (!window.disable_signup_autofocus) {
$(el_window)
.find('.username')
.get(0)
.focus({ preventScroll: true });
}
wireAuthLogoHeader(el_window);
if (logo_clickable) {
$(el_window)
.find('.auth-logo')
.on('click', function () {
// keep desktop users on the desktop; everyone else goes to the root dashboard
window.location.href =
window.location.pathname === '/desktop'
? '/desktop'
: '/';
});
}
// Initialize Turnstile widget with callback to capture token
const initTurnstile = () => {
if (
window.turnstile &&
window.gui_params?.turnstileSiteKey
) {
window.turnstile.render('.cf-turnstile', {
sitekey: window.gui_params.turnstileSiteKey,
callback: function (token) {
// Store the token for the signup request
$(el_window)
.find('.cf-turnstile')
.attr('data-token', token);
// Enable the signup button once CAPTCHA is completed
$(el_window)
.find('.signup-btn')
.prop('disabled', false);
// Add visual feedback
$(el_window)
.find('.cf-turnstile')
.addClass('captcha-completed');
},
'expired-callback': function () {
// Reset when token expires
$(el_window)
.find('.cf-turnstile')
.removeAttr('data-token');
$(el_window)
.find('.cf-turnstile')
.removeClass('captcha-completed');
$(el_window)
.find('.signup-btn')
.prop('disabled', true);
},
});
} else {
// If Turnstile isn't loaded yet, wait for it
setTimeout(initTurnstile, 100);
}
};
initTurnstile();
if (window.signup_bonus_code) {
(async () => {
let fingerprint = null;
try {
fingerprint =
await window.getDeviceFingerprint?.();
} catch (_) {
// the check works without device signals
}
const result = await checkSignupBonusCode(
window.signup_bonus_code,
{ origin: window.gui_origin, fingerprint },
);
// Unknown (null) keeps the code; signup has the final say.
if (result && !result.valid) {
window.signup_bonus_code = null;
}
renderBonusNotice(el_window, result);
})();
}
(async () => {
try {
// A federated hop navigates this popup away and the
// provider returns it as a plain sign-in popup, losing
// whatever the popup was opened to do — and, for a
// permission prompt, handing the opener a token instead
// of a decision. Don't offer it there.
if (
window.embedded_in_popup &&
!offersFederatedSignInInPopup(window.gui_action)
) {
return;
}
const res = await fetch(
`${window.api_origin}/auth/oidc/providers`,
);
if (!res.ok) return;
const data = await res.json();
if (!data.providers) return;
const wireOidcBtn = (provider, btnClass) => {
$(el_window).find(btnClass).css('display', 'flex');
$(el_window)
.find(btnClass)
.on('click', function () {
let url = `${window.gui_origin}/auth/oidc/${provider}/start?flow=signup`;
// return to the interface the signup started from (backend whitelists the path)
const return_to = get_oidc_return_to();
if (return_to) {
url += `&return_to=${encodeURIComponent(return_to)}`;
}
const referrer =
options.referrer ?? window.referrerStr;
if (referrer) {
url += `&referrer=${encodeURIComponent(referrer)}`;
}
if (window.signup_bonus_code) {
url += `&bonusCode=${encodeURIComponent(window.signup_bonus_code)}`;
}
if (
window.embedded_in_popup &&
window.url_query_params?.get('msg_id')
) {
url += `&embedded_in_popup=true&msg_id=${encodeURIComponent(window.url_query_params.get('msg_id'))}`;
if (window.openerOrigin) {
url += `&opener_origin=${encodeURIComponent(window.openerOrigin)}`;
}
}
window.location.href = url;
});
};
let hasProvider = false;
if (data.providers.includes('google')) {
hasProvider = true;
wireOidcBtn('google', '.oidc-google-btn');
}
if (data.providers.includes('apple')) {
hasProvider = true;
wireOidcBtn('apple', '.oidc-apple-btn');
}
if (data.providers.includes('microsoft')) {
hasProvider = true;
wireOidcBtn('microsoft', '.oidc-microsoft-btn');
}
// Any other configured provider (e.g. a self-hosted
// admin's custom OIDC IdP) gets a generic button.
data.providers
.filter(
provider =>
!KNOWN_OIDC_PROVIDERS.includes(provider),
)
.forEach((provider, idx) => {
hasProvider = true;
const btnClass = `oidc-custom-btn-${idx}`;
const label =
humanizeOidcProviderId(provider);
$(el_window)
.find('.oidc-custom-providers')
.append(
`<button type="button" class="${btnClass} button button-block button-normal" style="display:flex; align-items:center; justify-content:center; gap:8px; margin-top:8px;">${OIDC_GENERIC_PROVIDER_ICON}${i18n('sign_up_with_provider', [label])}</button>`,
);
wireOidcBtn(provider, `.${btnClass}`);
});
if (hasProvider) {
$(el_window).find('.oidc-providers-wrapper').show();
// Hide the signup form and show the "Sign up with email" button
$(el_window).find('.signup-form').hide();
$(el_window).find('.signup-form-title').hide();
$(el_window)
.find('.signup-with-email-wrapper')
.show();
$(el_window)
.find('.signup-with-email-btn')
.on('click', function () {
$(el_window).find('.signup-form').show();
$(el_window)
.find('.signup-form-title')
.show();
$(el_window)
.find('.signup-with-email-wrapper')
.hide();
$(el_window)
.find('.username')
.get(0)
?.focus({ preventScroll: true });
});
}
} catch (_) {}
})();
},
window_class: 'window-signup',
window_css: {
height: 'initial',
},
body_css: {
width: 'initial',
'background-color': 'white',
'backdrop-filter': 'blur(3px)',
display: 'flex',
'flex-direction': 'column',
'justify-content': 'center',
'align-items': 'center',
padding: '20px 10px 10px 10px',
},
// Add custom CSS for CAPTCHA states
custom_css: `
.cf-turnstile.captcha-completed {
border: 2px solid #4CAF50;
border-radius: 4px;
padding: 2px;
}
.signup-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`,
});
$(el_window)
.find('.login-c2a-clickable')
.on('click', async function (e) {
$('.login-c2a-clickable').parents('.window').close();
const login = await UIWindowLogin({
referrer: options.referrer,
reload_on_success: options.reload_on_success,
redirect_url: options.redirect_url,
window_options: options.window_options,
show_close_button: options.show_close_button,
send_confirmation_code: options.send_confirmation_code,
show_password: false,
});
if (login) {
resolve(true);
}
});
$(el_window)
.find('.signup-btn')
.on('click', async function (e) {
// Clear previous error states
$(el_window).find('.signup-error-msg').hide();
//Username
let username = $(el_window).find('.username').val();
if (!username) {
$(el_window)
.find('.signup-error-msg')
.html(i18n('username_required'));
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
//Email
let email = $(el_window).find('.email').val();
// must have an email
if (!email) {
$(el_window)
.find('.signup-error-msg')
.html(i18n('email_required'));
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
// must be a valid email
else if (!window.is_email(email)) {
$(el_window)
.find('.signup-error-msg')
.html(i18n('email_invalid'));
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
//Password
let password = $(el_window).find('.password').val();
// must have a password
if (!password) {
$(el_window)
.find('.signup-error-msg')
.html(i18n('password_required'));
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
// check password strength
const pass_strength = check_password_strength(password);
if (!pass_strength.overallPass) {
$(el_window)
.find('.signup-error-msg')
.html(i18n('password_strength_error'));
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
// get confirm password value
const confirmPassword = $(el_window)
.find('.confirm-password')
.val();
if (!confirmPassword) {
$(el_window)
.find('.signup-error-msg')
.html(i18n('confirm_password_required'));
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
// check if passwords match
if (password !== confirmPassword) {
$(el_window)
.find('.signup-error-msg')
.html(i18n('passwords_do_not_match'));
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
// Check if Cloudflare Turnstile CAPTCHA was completed
let turnstileToken = null;
if (window.turnstile && window.gui_params?.turnstileSiteKey) {
turnstileToken = $(el_window)
.find('.cf-turnstile')
.attr('data-token');
if (!turnstileToken) {
$(el_window)
.find('.signup-error-msg')
.html(
i18n('captcha_required') ||
'Please complete the CAPTCHA verification',
);
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
}
//xyzname
let p102xyzname = $(el_window).find('.p102xyzname').val();
// disable 'Create Account' button
$(el_window).find('.signup-btn').prop('disabled', true);
let headers = {};
if (window.custom_headers) {
headers = window.custom_headers;
}
// Device signal for abuse prevention; omitted when unavailable
let fingerprint = null;
try {
fingerprint = await window.getDeviceFingerprint?.();
} catch (_) {
// signup must never block or fail because of device signals
}
// Include captcha in request only if required
const requestData = {
username: username,
email: email,
password: password,
referrer: options.referrer ?? window.referrerStr,
send_confirmation_code: options.send_confirmation_code,
p102xyzname: p102xyzname,
'cf-turnstile-response': turnstileToken,
};
if (fingerprint) {
requestData.fingerprint = fingerprint;
}
if (window.signup_bonus_code) {
requestData.bonusCode = window.signup_bonus_code;
}
$.ajax({
url: `${window.gui_origin}/signup`,
type: 'POST',
async: true,
headers: headers,
contentType: 'application/json',
data: JSON.stringify(requestData),
success: async function (data) {
await window.update_auth_data(data.token, data.user);
let email_verified = true;
const show_email_dialog =
!options.reload_on_success &&
(options.send_confirmation_code ||
data.user?.requires_email_confirmation);
if (
data.user?.requires_phone_verification ||
data.user?.requires_card_verification ||
show_email_dialog
) {
$(el_window).close();
// Verification gates run in order: email →
// phone (SMS) → card, matching the server-side
// order in assertVerifiedAccount.
if (show_email_dialog) {
email_verified =
await UIWindowEmailConfirmationRequired({
stay_on_top: true,
has_head: true,
reload_on_success:
options.reload_on_success,
window_options:
options.window_options ?? {},
});
}
// A user the SMS path keeps failing may take the
// card fallback instead; that resolves 'card' and
// clears the card gate along with the phone gate
// (see UIWindowPhoneVerificationRequired).
let card_gate_cleared_by_fallback = false;
if (data.user?.requires_phone_verification) {
let phone_ok = false;
do {
phone_ok =
await UIWindowPhoneVerificationRequired(
{
show_close_button: false,
stay_on_top: true,
has_head: true,
window_options:
options.window_options ??
{},
},
);
} while (!phone_ok);
card_gate_cleared_by_fallback =
phone_ok === 'card';
}
// Card verification is the last gate.
if (
data.user?.requires_card_verification &&
!card_gate_cleared_by_fallback
) {
let card_ok = false;
do {
card_ok =
await UIWindowCardVerificationRequired({
show_close_button: false,
stay_on_top: true,
has_head: true,
window_options:
options.window_options ?? {},
});
} while (!card_ok);
}
}
if (options.reload_on_success) {
window.onbeforeunload = null;
// Signup can interrupt a direct app landing (the
// login cover page's "Sign up", the session
// picker's "Create Account", the
// must_login_or_signup fallback) — landing on '/'
// afterwards would silently drop the app the URL
// asked for, so app landings are preserved and
// the reloaded boot replays them, launch and
// dashboard intro included (the query string is
// deliberately left behind, mirroring login's
// credential-leak hygiene). Every other route
// keeps the historical '/' — notably
// /action/signup, where coming back to the same
// path would just show this window again.
const on_app_landing =
/^\/(?:desktop\/)?app\/[^/]+\/?$/.test(
window.location.pathname,
);
const redirectUrl =
options.redirect_url ||
(on_app_landing
? window.location.origin +
window.location.pathname
: '/');
window.location.replace(redirectUrl);
} else {
resolve(email_verified);
}
},
error: function (err) {
// re-enable 'Create Account' button so user can try again
$(el_window)
.find('.signup-btn')
.prop('disabled', false);
// Reset Turnstile widget for retry
try {
if (window.turnstile) {
window.turnstile?.reset('.cf-turnstile');
$(el_window)
.find('.cf-turnstile')
.removeAttr('data-token');
$(el_window)
.find('.cf-turnstile')
.removeClass('captcha-completed');
}
} catch (e) {
console.log(e);
}
// Process error response
const errorText = err.responseText || '';
// Handle JSON error response
try {
// Try to parse error as JSON
const errorJson = JSON.parse(errorText);
// Handle signup blocked with full-screen overlay
if (errorJson?.code === 'signup_blocked') {
const overlay = document.createElement('div');
overlay.classList.add('signup-blocked-overlay');
const blockedMsg =
errorJson.message || 'Signup Not Allowed';
overlay.innerHTML = `
<div class="signup-blocked-content">
<img src="${window.icons['logo.svg'] || window.icons['logo-white.svg'] || ''}" style="width:64px;margin-bottom:24px;" />
<p>${html_encode(blockedMsg)}</p>
<p>If you already have an account, try <a href="/action/login">logging in</a>. Otherwise, contact <a href="mailto:support@puter.com">support@puter.com</a> for assistance.</p>
</div>
`;
document.body.appendChild(overlay);
return;
}
// Drop the code so the next attempt signs up without it.
if (errorJson?.code === 'bonus_code_invalid') {
window.signup_bonus_code = null;
// The error below says it; the offer no longer applies.
renderBonusNotice(el_window, null);
$(el_window)
.find('.signup-error-msg')
.html(i18n('signup_bonus_code_invalid_retry'));
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
// Handle timeout specifically
if (
errorJson?.code === 'response_timeout' ||
errorText.includes('timeout')
) {
$(el_window)
.find('.signup-error-msg')
.html(
i18n('server_timeout') ||
'The server took too long to respond. Please try again.',
);
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
// If it's a message in the JSON, use that
if (errorJson.message) {
$(el_window)
.find('.signup-error-msg')
.html(errorJson.message);
$(el_window).find('.signup-error-msg').fadeIn();
return;
}
} catch (e) {
console.log(e);
// Not JSON, continue with text analysis
}
// Default general error handling
$(el_window)
.find('.signup-error-msg')
.html(
errorText ||
i18n('signup_error') ||
'An error occurred during signup. Please try again.',
);
$(el_window).find('.signup-error-msg').fadeIn();
},
timeout: 30000, // Add a reasonable timeout
});
});
$(el_window)
.find('.signup-form')
.on('submit', function (e) {
e.preventDefault();
e.stopPropagation();
return false;
});
$(el_window)
.find(
`#toggle-show-password-${internal_id}, #toggle-show-password-${internal_id}-confirm`,
)
.on('click', function (e) {
// hide/show password/confirm password and update icon
let inputField = $(this).siblings('input');
let isPasswordVisible = inputField.attr('type') === 'text';
inputField.attr(
'type',
isPasswordVisible ? 'password' : 'text',
);
$(this)
.find('.toggle-show-password-icon')
.attr(
'src',
isPasswordVisible
? window.icons['eye-open.svg']
: window.icons['eye-closed.svg'],
);
});
//remove login window
$('.signup-c2a-clickable').parents('.window').close();
});
}
export default UIWindowSignup;