{"id":16,"date":"2026-03-20T16:53:08","date_gmt":"2026-03-20T16:53:08","guid":{"rendered":"https:\/\/spca.org.sg\/dev\/vms\/?page_id=16"},"modified":"2026-04-12T12:07:43","modified_gmt":"2026-04-12T04:07:43","slug":"volunteer-portal","status":"publish","type":"page","link":"https:\/\/spca.org.sg\/vms\/","title":{"rendered":"Volunteer Portal"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"16\" class=\"elementor elementor-16\">\n\t\t\t\t<div class=\"elementor-element elementor-element-5ad7f66 e-flex e-con-boxed e-con e-parent\" data-id=\"5ad7f66\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-eeedd03 elementor-widget elementor-widget-vms_portal\" data-id=\"eeedd03\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"vms_portal.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<div id=\"vms-login-wrap\" class=\"vms-form-wrap vms-login-wrap\">\n\n    <h2 class=\"vms-form-title\" id=\"vms-login-title\">Volunteer Login<\/h2>\n\n    <div id=\"vms-login-messages\" class=\"vms-messages\" style=\"display:none;\"><\/div>\n\n    <!-- Step 1: Email + Password -->\n    <form id=\"vms-login-form\" method=\"post\" novalidate>\n        <input type=\"hidden\" name=\"nonce\"        value=\"04218bd6d2\">\n        <input type=\"hidden\" name=\"action\"       value=\"vms_login\">\n        <input type=\"hidden\" name=\"redirect_url\" value=\"https:\/\/spca.org.sg\/vms\/\">\n\n        <div class=\"vms-field\">\n            <label for=\"vms_login_email\">Email Address <span class=\"required\">*<\/span><\/label>\n            <input type=\"email\" id=\"vms_login_email\" name=\"email\" required\n                   autocomplete=\"email\" placeholder=\"your@email.com\">\n        <\/div>\n\n        <div class=\"vms-field\">\n            <label for=\"vms_login_password\">Password <span class=\"required\">*<\/span><\/label>\n            <input type=\"password\" id=\"vms_login_password\" name=\"password\" required autocomplete=\"current-password\">\n            <div class=\"vms-field-inline\" style=\"margin-top:6px\">\n                <input type=\"checkbox\" id=\"vms_show_login_pw\" class=\"vms-show-password\" data-target=\"vms_login_password\">\n                <label for=\"vms_show_login_pw\" class=\"vms-inline-label\">Show password<\/label>\n            <\/div>\n        <\/div>\n\n        <div class=\"vms-field vms-field-inline\">\n            <input type=\"checkbox\" id=\"vms_remember_me\" name=\"remember_me\" value=\"1\">\n            <label for=\"vms_remember_me\" class=\"vms-inline-label\">Keep me logged in<\/label>\n        <\/div>\n\n        <div class=\"vms-submit-row\" style=\"display:flex;gap:20px;\">\n            <button type=\"submit\" id=\"vms-login-submit\" class=\"vms-btn vms-btn-primary\" style=\"flex:1;width:auto;\">\n                Log In            <\/button>\n            <button type=\"button\" id=\"vms-magic-link-toggle\" class=\"vms-btn\" style=\"flex:1;width:auto;background:#ea580c;border-color:#ea580c;color:#fff;\">\n                Send Login Link            <\/button>\n        <\/div>\n\n        <div class=\"vms-login-links\">\n            <a href=\"https:\/\/spca.org.sg\/vms\/wp-login.php?action=lostpassword&#038;redirect_to=https%3A%2F%2Fspca.org.sg%2Fvms%2F\">\n                Forgot your password?            <\/a>\n            <span class=\"vms-link-divider\">&middot;<\/span>\n            <a href=\"https:\/\/spca.org.sg\/vms\/volunteer-registration\/\">\n                Not registered yet? Sign up            <\/a>\n        <\/div>\n    <\/form>\n\n    <!-- Magic Link Request Panel -->\n    <div id=\"vms-magic-panel\" style=\"display:none;\">\n        <p style=\"margin:0 0 14px;font-size:14px;color:#555;\">\n            Enter your email address and we will send you a one-click login link valid for 72 hours.        <\/p>\n\n        <div id=\"vms-magic-messages\" class=\"vms-messages\" style=\"display:none;\"><\/div>\n\n        <div class=\"vms-field\">\n            <label for=\"vms_magic_email\">Email Address <span class=\"required\">*<\/span><\/label>\n            <input type=\"email\" id=\"vms_magic_email\" name=\"magic_email\" required\n                   autocomplete=\"email\" placeholder=\"your@email.com\">\n        <\/div>\n\n        <div class=\"vms-submit-row\">\n            <button type=\"button\" id=\"vms-magic-submit\" class=\"vms-btn vms-btn-primary vms-btn-full\">\n                Send Login Link            <\/button>\n        <\/div>\n\n        <div class=\"vms-login-links\">\n            <a href=\"#\" id=\"vms-magic-back-btn\">&larr; Back to login<\/a>\n        <\/div>\n    <\/div>\n\n    <!-- Step 2: 2FA OTP Challenge (hidden until needed) -->\n    <div id=\"vms-tfa-panel\" style=\"display:none;\">\n        <p id=\"vms-tfa-instruction\" class=\"vms-tfa-instruction\"><\/p>\n\n        <div class=\"vms-field\">\n            <label for=\"vms-tfa-code\">Verification Code<\/label>\n            <input type=\"text\" id=\"vms-tfa-code\" maxlength=\"6\" pattern=\"[0-9]{6}\"\n                   autocomplete=\"one-time-code\" placeholder=\"000000\" class=\"vms-otp-input\">\n        <\/div>\n\n        <div id=\"vms-tfa-resend-wrap\" style=\"display:none;\">\n            <button type=\"button\" id=\"vms-tfa-resend-btn\" class=\"vms-btn vms-btn-ghost vms-btn-sm\">\n                Resend Code            <\/button>\n        <\/div>\n\n        <div class=\"vms-submit-row\">\n            <button type=\"button\" id=\"vms-tfa-verify-btn\" class=\"vms-btn vms-btn-primary vms-btn-full\">\n                Verify            <\/button>\n        <\/div>\n\n        <div class=\"vms-login-links\">\n            <a href=\"#\" id=\"vms-tfa-back-btn\">&larr; Back to login<\/a>\n        <\/div>\n    <\/div>\n\n<\/div>\n\n<script>\n(function ($) {\n    var ajaxUrl    = 'https:\/\/spca.org.sg\/vms\/wp-admin\/admin-ajax.php';\n    var frontNonce = 'df902b2fb8';\n    var tfaToken   = '';\n    var tfaMethod  = '';\n\n    \/\/ \u2500\u2500 Fresh-nonce helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    \/\/ The nonces above were generated when this page was rendered. If the page\n    \/\/ was served from a cache (browser, CDN, WP Rocket, etc.) those nonces may\n    \/\/ be stale, causing \"Security check failed\" errors on submit. This helper\n    \/\/ fetches a fresh pair from a no-cache endpoint and updates the form's\n    \/\/ hidden input AND the JS variable before the actual submit fires.\n    \/\/\n    \/\/ Adds ~50-100ms of latency per submit \u2014 negligible vs. the alternative of\n    \/\/ users hitting the security-check error and having to refresh.\n    function refreshNonces(callback) {\n        $.post(ajaxUrl, { action: 'vms_get_login_nonces' }, function (res) {\n            if (res && res.success && res.data) {\n                $('input[name=\"nonce\"][value]').val(res.data.login_nonce);\n                frontNonce = res.data.front_nonce;\n            }\n            callback();\n        }).fail(function () {\n            \/\/ Even if the refresh fails, attempt submission with the\n            \/\/ original nonces \u2014 they might still be valid.\n            callback();\n        });\n    }\n\n    \/\/ Show\/hide password \u2014 checkbox based.\n    $(document).on('change', '.vms-show-password', function () {\n        $('#' + $(this).data('target')).attr('type', $(this).is(':checked') ? 'text' : 'password');\n    });\n\n    function showMsg(msg, type) {\n        var cls = type === 'success' ? 'vms-success' : 'vms-error';\n        $('#vms-login-messages').html('<div class=\"' + cls + '\">' + msg + '<\/div>').show();\n    }\n\n    function showTfaPanel(method, token, instruction) {\n        tfaToken  = token;\n        tfaMethod = method;\n        $('#vms-login-form').hide();\n        $('#vms-login-title').text('Verification Required');\n        $('#vms-tfa-instruction').text(instruction);\n        $('#vms-tfa-code').val('');\n        \/\/ Show resend button for email\/telegram only.\n        if (method === 'email' || method === 'telegram') {\n            $('#vms-tfa-resend-wrap').show();\n        } else {\n            $('#vms-tfa-resend-wrap').hide();\n        }\n        $('#vms-login-messages').hide();\n        $('#vms-tfa-panel').show();\n        $('#vms-tfa-code').focus();\n    }\n\n    \/\/ Step 1: Login form submission.\n    $('#vms-login-form').on('submit', function (e) {\n        e.preventDefault();\n        var $form = $(this);\n        var $btn  = $('#vms-login-submit');\n        var $msgs = $('#vms-login-messages');\n\n        $btn.prop('disabled', true).text('Logging in\\u2026');\n        $msgs.hide();\n\n        \/\/ Refresh nonces first \\u2014 handles the stale-page-cache case.\n        refreshNonces(function () {\n            $.post(ajaxUrl, $form.serialize(), function (res) {\n                if (res.success) {\n                    if (res.data.tfa_required) {\n                        $btn.prop('disabled', false).text('Log In');\n                        showTfaPanel(res.data.tfa_method, res.data.tfa_token, res.data.message);\n                    } else {\n                        $msgs.html('<div class=\"vms-success\">Login successful \\u2014 redirecting\\u2026<\/div>').show();\n                        window.location.href = res.data.redirect;\n                    }\n                } else {\n                    $msgs.html('<div class=\"vms-error\">' + (res.data.message || 'Login failed. Please check your details and try again.') + '<\/div>').show();\n                    $btn.prop('disabled', false).text('Log In');\n                }\n            }).fail(function () {\n                $msgs.html('<div class=\"vms-error\">A server error occurred. Please try again.<\/div>').show();\n                $btn.prop('disabled', false).text('Log In');\n            });\n        });\n    });\n\n    \/\/ Step 2: OTP verification.\n    $('#vms-tfa-verify-btn').on('click', function () {\n        var code  = $('#vms-tfa-code').val().trim();\n        var $btn  = $(this);\n        var $msgs = $('#vms-login-messages');\n\n        if (!code || code.length !== 6) {\n            showMsg('Please enter the 6-digit verification code.', 'error');\n            return;\n        }\n\n        $btn.prop('disabled', true).text('Verifying\\u2026');\n        $msgs.hide();\n\n        $.post(ajaxUrl, {\n            action:    'vms_tfa_verify',\n            nonce:     frontNonce,\n            tfa_token: tfaToken,\n            code:      code\n        }, function (res) {\n            if (res.success) {\n                $msgs.html('<div class=\"vms-success\">Verified \\u2014 redirecting\\u2026<\/div>').show();\n                window.location.href = res.data.redirect;\n            } else {\n                showMsg(res.data.message || 'Incorrect code. Please try again.', 'error');\n                $btn.prop('disabled', false).text('Verify');\n                $('#vms-tfa-code').val('').focus();\n            }\n        }).fail(function () {\n            showMsg('A server error occurred. Please try again.', 'error');\n            $btn.prop('disabled', false).text('Verify');\n        });\n    });\n\n    \/\/ Allow pressing Enter in OTP field.\n    $('#vms-tfa-code').on('keypress', function (e) {\n        if (e.which === 13) { $('#vms-tfa-verify-btn').trigger('click'); }\n    });\n\n    \/\/ Resend OTP.\n    $('#vms-tfa-resend-btn').on('click', function () {\n        var $btn  = $(this);\n        var $msgs = $('#vms-login-messages');\n        $btn.prop('disabled', true).text('Sending\\u2026');\n        $msgs.hide();\n        $.post(ajaxUrl, {\n            action:    'vms_tfa_send_otp',\n            nonce:     frontNonce,\n            tfa_token: tfaToken\n        }, function (res) {\n            if (res.success) {\n                showMsg(res.data.message || 'Code resent.', 'success');\n            } else {\n                showMsg(res.data.message || 'Could not resend code.', 'error');\n            }\n            $btn.prop('disabled', false).text('Resend Code');\n        });\n    });\n\n    \/\/ Back to login (from 2FA panel).\n    $('#vms-tfa-back-btn').on('click', function (e) {\n        e.preventDefault();\n        tfaToken = '';\n        tfaMethod = '';\n        $('#vms-tfa-panel').hide();\n        $('#vms-login-title').text('Volunteer Login');\n        $('#vms-login-messages').hide();\n        $('#vms-login-form').show();\n    });\n\n    \/\/ Toggle to magic link panel.\n    $('#vms-magic-link-toggle').on('click', function (e) {\n        e.preventDefault();\n        $('#vms-login-form').hide();\n        $('#vms-login-messages').hide();\n        $('#vms-login-title').text('Send Login Link');\n        $('#vms-magic-panel').show();\n        $('#vms_magic_email').focus();\n    });\n\n    \/\/ Back to login (from magic link panel).\n    $('#vms-magic-back-btn').on('click', function (e) {\n        e.preventDefault();\n        $('#vms-magic-panel').hide();\n        $('#vms-magic-messages').hide();\n        $('#vms-login-title').text('Volunteer Login');\n        $('#vms-login-form').show();\n    });\n\n    \/\/ Send magic link request.\n    $('#vms-magic-submit').on('click', function () {\n        var email = $('#vms_magic_email').val().trim();\n        var $btn  = $(this);\n        var $msgs = $('#vms-magic-messages');\n\n        if (!email) {\n            $msgs.html('<div class=\"vms-error\">Please enter your email address.<\/div>').show();\n            return;\n        }\n\n        $btn.prop('disabled', true).text('Sending\u2026');\n        $msgs.hide();\n\n        refreshNonces(function () {\n            $.post(ajaxUrl, {\n                action: 'vms_request_magic_link',\n                nonce:  frontNonce,\n                email:  email\n            }, function (res) {\n                \/\/ Surface real errors (nonce failure, server issues) so users\n                \/\/ aren't told \"you'll get an email\" when nothing was sent.\n                \/\/ For all OTHER cases (email not found, etc.) we keep the\n                \/\/ anti-enumeration generic success message.\n                if (res && !res.success && res.data && res.data.code === 'nonce_failed') {\n                    $msgs.html(\n                        '<div class=\"vms-error\">' +\n                        (res.data.message || 'Security check failed. Please refresh the page and try again.') +\n                        '<\/div>'\n                    ).show();\n                } else {\n                    $msgs.html('<div class=\"vms-success\">If that email is registered, you will receive a login link shortly. Please check your inbox.<\/div>').show();\n                    $('#vms_magic_email').val('');\n                }\n                $btn.prop('disabled', false).text('Send Login Link');\n            }).fail(function () {\n                \/\/ True network failure \u2014 surface this so the user knows to retry.\n                $msgs.html('<div class=\"vms-error\">Could not reach the server. Please check your connection and try again.<\/div>').show();\n                $btn.prop('disabled', false).text('Send Login Link');\n            });\n        });\n    });\n\n    \/\/ Enter key in magic email field.\n    $('#vms_magic_email').on('keypress', function (e) {\n        if (e.which === 13) { $('#vms-magic-submit').trigger('click'); }\n    });\n\n    \/\/ Show expired-link notice on page load.\n    \n    \/\/ Show password-reset-email-sent notice (?reset_sent=1 from WP forgot-password flow).\n    \n})(jQuery);\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Volunteer record not found. Please contact the administrator.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-16","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/spca.org.sg\/vms\/wp-json\/wp\/v2\/pages\/16","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/spca.org.sg\/vms\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/spca.org.sg\/vms\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/spca.org.sg\/vms\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/spca.org.sg\/vms\/wp-json\/wp\/v2\/comments?post=16"}],"version-history":[{"count":22,"href":"https:\/\/spca.org.sg\/vms\/wp-json\/wp\/v2\/pages\/16\/revisions"}],"predecessor-version":[{"id":379,"href":"https:\/\/spca.org.sg\/vms\/wp-json\/wp\/v2\/pages\/16\/revisions\/379"}],"wp:attachment":[{"href":"https:\/\/spca.org.sg\/vms\/wp-json\/wp\/v2\/media?parent=16"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}