Det här är den korta, praktiska guiden för att låta ett vBulletin-hookat frontend-script prata direkt med Tools efter att forumregistreringen ser klar ut.
Målet är att slippa skicka användaren genom hela den publika onboarding-sidan när forumet redan känner till användaren och invite-koden.
Publik endpoint:
POST /api/vbulletin/onboarding/invite/completeExempel:
https://tools.tornevall.net/api/vbulletin/onboarding/invite/completehttps://tools.tornevall.com/api/vbulletin/onboarding/invite/completeDen här endpointen använder inga bearer tokens och ska inte skyddas med något separat frontend-token.
Säkerhetsmodellen här bygger i stället på:
field66Request-body:
{
"invite_code": "invite-code-here",
"forum_user_id": 123,
"forum_username": "snowrider",
"forum_email": "user@example.com"
}
Regler:
invite_code är obligatoriskforum_user_id är den starkaste extra identitetshinten när den finnsforum_username är bra som fallbackforum_email är bara en hint och räknas inte ensam som bevisOm invitefältet i forumet råkar innehålla en hel URL i stället för bara själva koden, så normaliserar Tools nu detta server-side.
Det betyder att följande fortfarande fungerar:
https://tools.tornevall.net/vbulletin/onboarding/snowboarding/ABC123
Tools försöker då plocka ut själva invitekoden (ABC123) innan lookupen körs.
Det är ändå klokt att även rensa värdet client-side innan det skickas.
För den aktuella hooken i vBulletin:
header_headsnowball_header_scriptHook arguments:
userinfo=userinfo
bbuserinfo=bbuserinfo
Det gör att scriptet kan läsa vanliga användarfält från både userinfo och bbuserinfo i templaten.
Din nuvarande bootstrapdel bör anpassas så att den klarar både tomma värden och URL-baserade invitefält.
Exempel:
<script>
(function () {
function firstNonEmpty() {
for (var i = 0; i < arguments.length; i += 1) {
var value = String(arguments[i] || '').trim();
if (value !== '') {
return value;
}
}
return '';
}
window.TornevallTools = window.TornevallTools || {};
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
window.TornevallTools.vbulletin.user = window.TornevallTools.vbulletin.user || {};
window.TornevallTools.vbulletin.user.userid = firstNonEmpty(
"{vb:raw bbuserinfo.userid}",
"{vb:raw userinfo.userid}"
);
window.TornevallTools.vbulletin.user.username = firstNonEmpty(
"{vb:raw bbuserinfo.username}",
"{vb:raw userinfo.username}"
);
window.TornevallTools.vbulletin.user.invitationCodeRaw = firstNonEmpty(
"{vb:raw bbuserinfo.field66}",
"{vb:raw userinfo.field66}"
);
window.TornevallTools.vbulletin.user.invitationFieldId = 66;
})();
</script>
Använd en liten helper innan API-anropet görs.
<script>
(function () {
function normalizeInviteCode(rawValue) {
var value = String(rawValue || '').trim();
if (!value) {
return '';
}
try {
var parsed = new URL(value, window.location.origin);
var fromQuery = parsed.searchParams.get('invite_code')
|| parsed.searchParams.get('inviteKey')
|| parsed.searchParams.get('invite_key');
if (fromQuery) {
return String(fromQuery).trim();
}
var segments = parsed.pathname.split('/').filter(Boolean);
var onboardingIndex = segments.indexOf('onboarding');
if (onboardingIndex !== -1 && segments.length > onboardingIndex + 2) {
return String(segments[onboardingIndex + 2] || '').trim();
}
} catch (error) {
// Inte en URL, använd råvärdet som det är.
}
return value;
}
window.TornevallTools = window.TornevallTools || {};
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
window.TornevallTools.vbulletin.normalizeInviteCode = normalizeInviteCode;
})();
</script>
Det här är den praktiska kärnan.
<script>
(function () {
var toolsApiBase = 'https://tools.tornevall.net';
function normalizeInviteCode(rawValue) {
if (
window.TornevallTools
&& window.TornevallTools.vbulletin
&& typeof window.TornevallTools.vbulletin.normalizeInviteCode === 'function'
) {
return window.TornevallTools.vbulletin.normalizeInviteCode(rawValue);
}
return String(rawValue || '').trim();
}
function getForumUser() {
return (
window.TornevallTools
&& window.TornevallTools.vbulletin
&& window.TornevallTools.vbulletin.user
) || {};
}
function completeInviteWithTools() {
var user = getForumUser();
var inviteCode = normalizeInviteCode(user.invitationCodeRaw || user.invitationCode || '');
var forumUserId = parseInt(user.userid, 10) || 0;
var forumUsername = String(user.username || '').trim();
if (!inviteCode || !forumUserId) {
return Promise.resolve(null);
}
return fetch(toolsApiBase + '/api/vbulletin/onboarding/invite/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
invite_code: inviteCode,
forum_user_id: forumUserId,
forum_username: forumUsername
})
}).then(function (response) {
return response.json().then(function (data) {
return {
ok: response.ok,
status: response.status,
data: data
};
});
});
}
window.TornevallTools = window.TornevallTools || {};
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
window.TornevallTools.vbulletin.completeInviteWithTools = completeInviteWithTools;
})();
</script>
När API:t svarar att åtkomsten faktiskt är klar (access_granted=true) kan du visa en tydlig popup.
<script>
(function () {
function showInviteSuccessPopup(message, continueUrl) {
var existing = document.getElementById('tools-invite-success-popup');
if (existing) {
existing.remove();
}
var overlay = document.createElement('div');
overlay.id = 'tools-invite-success-popup';
overlay.innerHTML = ''
+ '<div class="tools-invite-popup-backdrop"></div>'
+ '<div class="tools-invite-popup-card" role="dialog" aria-live="polite" aria-modal="true">'
+ ' <div class="tools-invite-popup-icon">✓</div>'
+ ' <h2>Grattis!</h2>'
+ ' <p>' + (message || 'Du är nu insläppt i forumet.') + '</p>'
+ ' <div class="tools-invite-popup-actions">'
+ (continueUrl
? ' <a class="tools-invite-popup-button" href="' + continueUrl + '">Fortsätt till forumet</a>'
: '')
+ ' <button type="button" class="tools-invite-popup-close">Stäng</button>'
+ ' </div>'
+ '</div>';
var style = document.createElement('style');
style.textContent = ''
+ '#tools-invite-success-popup { position: fixed; inset: 0; z-index: 2147483647; }'
+ '.tools-invite-popup-backdrop { position: absolute; inset: 0; background: rgba(10, 16, 25, 0.55); }'
+ '.tools-invite-popup-card {'
+ ' position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);'
+ ' width: min(92vw, 460px); padding: 28px 24px; border-radius: 18px;'
+ ' background: linear-gradient(180deg, #ffffff, #f5fbf7); box-shadow: 0 20px 60px rgba(0,0,0,0.25);'
+ ' text-align: center; font-family: Arial, sans-serif; color: #123226;'
+ '}'
+ '.tools-invite-popup-icon {'
+ ' width: 56px; height: 56px; margin: 0 auto 14px auto; border-radius: 50%;'
+ ' display: flex; align-items: center; justify-content: center;'
+ ' background: #2f9e68; color: #fff; font-size: 28px; font-weight: bold;'
+ '}'
+ '.tools-invite-popup-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 18px; }'
+ '.tools-invite-popup-button, .tools-invite-popup-close {'
+ ' appearance: none; border: 0; border-radius: 999px; padding: 12px 18px;'
+ ' background: #2f9e68; color: #fff; text-decoration: none; cursor: pointer; font-weight: 600;'
+ '}'
+ '.tools-invite-popup-close { background: #5b6770; }';
overlay.appendChild(style);
document.body.appendChild(overlay);
var closeButton = overlay.querySelector('.tools-invite-popup-close');
if (closeButton) {
closeButton.addEventListener('click', function () {
overlay.remove();
});
}
}
window.TornevallTools = window.TornevallTools || {};
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || {};
window.TornevallTools.vbulletin.showInviteSuccessPopup = showInviteSuccessPopup;
})();
</script>
<script>
(function () {
function runInviteCompletionFlow() {
if (!window.TornevallTools || !window.TornevallTools.vbulletin) {
return;
}
var api = window.TornevallTools.vbulletin;
if (typeof api.completeInviteWithTools !== 'function') {
return;
}
api.completeInviteWithTools().then(function (result) {
if (!result || !result.ok || !result.data || !result.data.ok) {
return;
}
if (result.data.access_granted && typeof api.showInviteSuccessPopup === 'function') {
var continueUrl = result.data.request && result.data.request.public_status_url
? result.data.request.public_status_url
: 'https://forum.tornevall.net';
api.showInviteSuccessPopup(
result.data.message || 'Grattis, du är nu insläppt.',
continueUrl
);
}
}).catch(function (error) {
if (window.console && console.warn) {
console.warn('Invite completion failed:', error);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', runInviteCompletionFlow);
} else {
runInviteCompletionFlow();
}
})();
</script>
access_granted=truerequires_manual_review=true404 eller 422header_head.userinfo=userinfobbuserinfo=bbuserinfofield66 som invitationCodeRaw.Det här är det nuvarande kompletta exemplet på hur aktiveringen kan ske i snowball_header_script.
Praktiskt beteende i detta exempel:
window.TornevallTools.vbulletin.userinviteCompleteEndpoint direkt i hookeninvitationCodeRaw och normaliserad invitationCode404 invite code not found som ett stopp-läge i stället för att spamma om samma fel under samma session<script>
(function () {
window.TornevallTools = window.TornevallTools || new Object();
window.TornevallTools.vbulletin = window.TornevallTools.vbulletin || new Object();
window.TornevallTools.vbulletin.user = window.TornevallTools.vbulletin.user || new Object();
window.TornevallTools.vbulletin.user.userid = "{vb:raw user.userid}";
window.TornevallTools.vbulletin.user.username = "{vb:raw user.username}";
window.TornevallTools.vbulletin.user.invitationCodeRaw = "{vb:raw user.field66}";
window.TornevallTools.vbulletin.user.invitationCode = "{vb:raw user.field66}";
window.TornevallTools.vbulletin.user.invitationFieldId = 66;
window.TornevallTools.vbulletin.user.inviteCompleteEndpoint = "https://tools.tornevall.net/api/vbulletin/onboarding/invite/complete";
})();
(function () {
var path = decodeURIComponent(window.location.pathname).toLowerCase();
if (path === "/snowball" || path === "/snowball/") {
window.location.replace("/snowball/forum");
return;
}
if (path === "/snowball/forum" || path.startsWith("/snowball/forum/")) {
document.documentElement.classList.add("is-snowball-forum");
}
})();
</script>
<style>
html.is-snowball-forum #header .site-logo a > img {
visibility: hidden !important;
}
#tools-vb-member-widgets {
max-width: 1080px;
margin: 0 auto 24px auto;
padding: 18px 22px;
border-left: 5px solid #6faf8b;
border-radius: 10px;
background: #f7fff9;
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.08);
text-align: center;
font-weight: 600;
}
#tools-vb-member-widgets .tools-vb-widget-line {
display: block;
margin: 4px 0;
}
#tools-vb-widget-loading {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
opacity: 0.75;
}
.tools-vb-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(0, 0, 0, 0.18);
border-top-color: rgba(0, 0, 0, 0.65);
border-radius: 50%;
animation: toolsVbSpin 0.8s linear infinite;
}
@keyframes toolsVbSpin {
to {
transform: rotate(360deg);
}
}
#tools-snowball-autoverify-popup {
position: fixed;
right: 22px;
bottom: 22px;
z-index: 999999;
max-width: 420px;
padding: 18px 20px;
border-left: 5px solid #6faf8b;
border-radius: 12px;
background: #f7fff9;
color: #1d2b20;
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.22);
font-family: inherit;
line-height: 1.45;
opacity: 0;
transform: translateY(14px);
transition: opacity 180ms ease, transform 180ms ease;
}
#tools-snowball-autoverify-popup.is-visible {
opacity: 1;
transform: translateY(0);
}
#tools-snowball-autoverify-popup.is-error {
border-left-color: #d0051b;
background: #fff7f7;
color: #3a1414;
}
#tools-snowball-autoverify-popup .tools-snowball-popup-title {
display: block;
margin-bottom: 6px;
padding-right: 24px;
font-size: 16px;
font-weight: 800;
}
#tools-snowball-autoverify-popup .tools-snowball-popup-message {
display: block;
padding-right: 24px;
font-size: 14px;
}
#tools-snowball-autoverify-popup .tools-snowball-popup-close {
position: absolute;
top: 7px;
right: 9px;
border: 0;
background: transparent;
color: inherit;
font-size: 20px;
line-height: 20px;
cursor: pointer;
opacity: 0.7;
}
#tools-snowball-autoverify-popup .tools-snowball-popup-close:hover {
opacity: 1;
}
</style>
<link rel="preload" as="image" href="https://forum.tornevall.net/filedata/fetch?photoid=1063354" fetchpriority="high">
<link rel="preconnect" href="https://tools.tornevall.net" crossorigin>
<link rel="preload" as="script" href="https://tools.tornevall.net/api/managed-scripts/vbulletin/bundle.js">
<link rel="preload" as="script" href="https://tools.tornevall.net/vbulletin/widgets/latest-member.js?group_ids=155&variant=plain&container_id=tools-vb-latest-member-widget&template=Vi+v%C3%A4lkomnar+v%C3%A5r+senaste+medlem+%7Bname%7D">
<link rel="preload" as="script" href="https://tools.tornevall.net/vbulletin/widgets/member-count.js?group_ids=155&variant=plain&container_id=tools-vb-member-count-widget&template=Sn%C3%B6bollseffekten+v%C3%A4xer+-+vi+%C3%A4r+nu+%7Bcount%7D+medlemmar">
<script defer src="https://tools.tornevall.net/api/managed-scripts/vbulletin/bundle.js"></script>
<script>
(function () {
var snowballLogoUrl = "https://forum.tornevall.net/filedata/fetch?photoid=1063354";
var path = decodeURIComponent(window.location.pathname).toLowerCase();
if (!(path === "/snowball/forum" || path.startsWith("/snowball/forum/"))) {
return;
}
var preloadedLogo = new Image();
preloadedLogo.fetchPriority = "high";
preloadedLogo.src = snowballLogoUrl;
function swapSnowballLogo() {
var logo = document.querySelector("#header .site-logo a > img");
if (!logo) {
return false;
}
logo.src = snowballLogoUrl;
logo.setAttribute("data-orig-src", snowballLogoUrl);
logo.alt = "Snöbollseffekten";
logo.title = "Snöbollseffekten";
logo.style.setProperty("visibility", "visible", "important");
return true;
}
if (!swapSnowballLogo()) {
var observer = new MutationObserver(function () {
if (swapSnowballLogo()) {
observer.disconnect();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
window.setTimeout(function () {
observer.disconnect();
}, 5000);
}
})();
(function () {
var user = window.TornevallTools
&& window.TornevallTools.vbulletin
&& window.TornevallTools.vbulletin.user
? window.TornevallTools.vbulletin.user
: new Object();
function trimString(value) {
return String(value || "").replace(/^\s+|\s+$/g, "");
}
function normalizeInviteCode(value) {
var raw = trimString(value);
if (!raw) {
return "";
}
try {
if (/^https?:\/\//i.test(raw)) {
var url = new URL(raw);
var parts = url.pathname.split("/");
var index = parts.length - 1;
while (index >= 0) {
if (parts[index]) {
return decodeURIComponent(parts[index]);
}
index--;
}
}
} catch (error) {
}
return raw;
}
function showSnowballPopup(title, message, isError) {
var existing = document.getElementById("tools-snowball-autoverify-popup");
if (existing && existing.parentNode) {
existing.parentNode.removeChild(existing);
}
var popup = document.createElement("div");
popup.id = "tools-snowball-autoverify-popup";
if (isError) {
popup.className = "is-error";
}
var closeButton = document.createElement("button");
closeButton.type = "button";
closeButton.className = "tools-snowball-popup-close";
closeButton.setAttribute("aria-label", "Stäng");
closeButton.appendChild(document.createTextNode("×"));
var titleNode = document.createElement("span");
titleNode.className = "tools-snowball-popup-title";
titleNode.appendChild(document.createTextNode(title));
var messageNode = document.createElement("span");
messageNode.className = "tools-snowball-popup-message";
messageNode.appendChild(document.createTextNode(message));
closeButton.onclick = function () {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
};
popup.appendChild(closeButton);
popup.appendChild(titleNode);
popup.appendChild(messageNode);
document.body.appendChild(popup);
window.setTimeout(function () {
popup.className = popup.className
? popup.className + " is-visible"
: "is-visible";
}, 20);
window.setTimeout(function () {
if (popup && popup.parentNode) {
popup.className = popup.className.replace(/\bis-visible\b/g, "");
}
}, 9000);
window.setTimeout(function () {
if (popup && popup.parentNode) {
popup.parentNode.removeChild(popup);
}
}, 9500);
}
function parseJsonResponse(response) {
return response.text().then(function (text) {
var payload = null;
try {
payload = text ? JSON.parse(text) : null;
} catch (error) {
payload = {
ok: false,
message: text || "Empty response",
raw: text || ""
};
}
if (!payload) {
payload = new Object();
}
payload.httpStatus = response.status;
payload.httpOk = response.ok;
return payload;
});
}
function isInviteNotFound(data) {
var message = trimString(data && data.message ? data.message : "").toLowerCase();
return data
&& Number(data.httpStatus) === 404
&& (
message === "invite code not found."
|| message === "invite code not found"
|| message.indexOf("invite code not found") !== -1
);
}
function isSuccessResponse(data) {
return data
&& (
data.ok === true
|| data.success === true
|| data.completed === true
|| data.status === "completed"
|| data.status === "ok"
|| data.status === "success"
);
}
function runSnowballAutoverify() {
var endpoint = trimString(user.inviteCompleteEndpoint || "https://tools.tornevall.net/api/vbulletin/onboarding/invite/complete");
var userid = trimString(user.userid || "");
var username = trimString(user.username || "");
var inviteCodeRaw = trimString(user.invitationCodeRaw || user.invitationCode || "");
var inviteCode = normalizeInviteCode(inviteCodeRaw);
if (window.console && console.log) {
console.log("Snowball autoverify boot:", {
endpoint: endpoint,
userid: userid,
username: username,
inviteCodeRaw: inviteCodeRaw,
inviteCode: inviteCode
});
}
if (!endpoint) {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: missing endpoint");
}
return;
}
if (!userid || userid === "0") {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: missing userid");
}
return;
}
if (!inviteCode) {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: missing invite code");
}
return;
}
var storageKey = "tools_snowball_invite_complete_" + userid + "_" + inviteCode;
try {
if (window.sessionStorage && sessionStorage.getItem(storageKey) === "done") {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: already done in this browser session");
}
return;
}
if (window.sessionStorage && sessionStorage.getItem(storageKey) === "not_found") {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: invite code was already reported as not found in this browser session");
}
return;
}
if (window.sessionStorage && sessionStorage.getItem(storageKey) === "running") {
if (window.console && console.info) {
console.info("Snowball autoverify skipped: already running");
}
return;
}
if (window.sessionStorage) {
sessionStorage.setItem(storageKey, "running");
}
} catch (error) {
}
var body = new FormData();
body.append("invite_code", inviteCode);
body.append("invite_code_raw", inviteCodeRaw);
body.append("forum_user_id", userid);
body.append("forum_username", username);
body.append("userid", userid);
body.append("username", username);
body.append("field_id", String(user.invitationFieldId || 66));
if (window.console && console.log) {
console.log("Snowball autoverify POST:", endpoint);
}
fetch(endpoint, {
method: "POST",
body: body,
credentials: "omit"
})
.then(parseJsonResponse)
.then(function (data) {
if (window.console && console.log) {
console.log("Snowball autoverify response:", data);
}
if (isSuccessResponse(data)) {
try {
if (window.sessionStorage) {
sessionStorage.setItem(storageKey, "done");
}
} catch (error) {
}
showSnowballPopup(
"Grattis, du är nu insläppt!",
"Din inbjudningskod är verifierad och forumåtkomsten har uppdaterats.",
false
);
return;
}
if (isInviteNotFound(data)) {
try {
if (window.sessionStorage) {
sessionStorage.setItem(storageKey, "not_found");
}
} catch (error) {
}
if (window.console && console.info) {
console.info("Snowball autoverify stopped: invite code not found in Tools.", {
inviteCode: inviteCode,
httpStatus: data.httpStatus,
message: data.message || ""
});
}
return;
}
try {
if (window.sessionStorage) {
sessionStorage.removeItem(storageKey);
}
} catch (error) {
}
if (window.console && console.info) {
console.info("Snowball autoverify did not complete:", data);
}
showSnowballPopup(
"Inbjudan kunde inte verifieras",
data && data.message ? data.message : "Tools API svarade, men bekräftade inte insläppet.",
true
);
})
.catch(function (error) {
try {
if (window.sessionStorage) {
sessionStorage.removeItem(storageKey);
}
} catch (storageError) {
}
if (window.console && console.warn) {
console.warn("Snowball autoverify network error:", error);
}
showSnowballPopup(
"Inbjudan kunde inte verifieras",
"Nätverksanropet till Tools kunde inte slutföras just nu.",
true
);
});
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", runSnowballAutoverify);
} else {
runSnowballAutoverify();
}
})();
</script>
invitationCodeRaw och den normaliserade invitekoden.