Release v2.20.1: migrate authentication to Better Auth
This commit is contained in:
1 parent
e9a53f577c
commit
fba4f99ff7
92 files changed
+5085
-1943
No files matched your search
@@ -447,6 +447,49 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-card .login-box-msg {
|
||||
margin: 0 0 1.25rem;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-card .form-label {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.login-card .input-group-text {
|
||||
min-width: 2.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-or-divider {
|
||||
align-items: center;
|
||||
color: var(--bs-secondary-color);
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.login-or-divider::before,
|
||||
.login-or-divider::after {
|
||||
background: var(--bs-border-color);
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 359.98px) {
|
||||
.login-card [data-login-actions] {
|
||||
align-items: flex-start !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-card [data-login-submit] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.email-verified-card {
|
||||
max-width: 26rem;
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
(function () {
|
||||
var currentPasswordInput = document.querySelector('[data-account-current-password]');
|
||||
var profileFormInput = document.querySelector('[data-account-profile-current-password]');
|
||||
var passwordFormInput = document.querySelector('[data-account-password-current-password]');
|
||||
var profileForm = profileFormInput && profileFormInput.form;
|
||||
var passwordForm = passwordFormInput && passwordFormInput.form;
|
||||
|
||||
if (!currentPasswordInput || !profileFormInput || !passwordFormInput || !profileForm || !passwordForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
function syncCurrentPassword() {
|
||||
profileFormInput.value = currentPasswordInput.value;
|
||||
passwordFormInput.value = currentPasswordInput.value;
|
||||
}
|
||||
|
||||
function clearCurrentPassword() {
|
||||
currentPasswordInput.value = '';
|
||||
currentPasswordInput.setCustomValidity('');
|
||||
currentPasswordInput.removeAttribute('required');
|
||||
profileFormInput.value = '';
|
||||
passwordFormInput.value = '';
|
||||
passwordForm.querySelectorAll('input[type="password"]').forEach(function (input) {
|
||||
input.value = '';
|
||||
input.setCustomValidity('');
|
||||
});
|
||||
profileForm.classList.remove('was-validated');
|
||||
passwordForm.classList.remove('was-validated');
|
||||
}
|
||||
|
||||
currentPasswordInput.addEventListener('input', syncCurrentPassword);
|
||||
currentPasswordInput.addEventListener('input', function () {
|
||||
currentPasswordInput.setCustomValidity('');
|
||||
});
|
||||
document.addEventListener('submit', function (event) {
|
||||
if (event.target !== profileForm && event.target !== passwordForm) {
|
||||
return;
|
||||
}
|
||||
if (!currentPasswordInput.value) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
currentPasswordInput.setCustomValidity('Current password is required.');
|
||||
currentPasswordInput.reportValidity();
|
||||
return;
|
||||
}
|
||||
currentPasswordInput.setCustomValidity('');
|
||||
syncCurrentPassword();
|
||||
}, true);
|
||||
passwordForm.addEventListener('submit', syncCurrentPassword);
|
||||
document.addEventListener('web-async-save:success', function (event) {
|
||||
if (event.detail && event.detail.form !== profileForm && event.detail.form !== passwordForm) {
|
||||
return;
|
||||
}
|
||||
clearCurrentPassword();
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
(function () {
|
||||
var client = window.pulsePasskeyAuth;
|
||||
var addForm = document.querySelector('[data-passkey-add]');
|
||||
var addTrigger = document.querySelector('[data-passkey-add-trigger]');
|
||||
var feedback = document.querySelector('[data-passkey-feedback]');
|
||||
var progress = document.querySelector('[data-passkey-progress]');
|
||||
var unavailable = document.querySelector('[data-passkey-unavailable]');
|
||||
var passkeyConfig = document.querySelector('[data-passkey-rp-id]');
|
||||
var passkeyRpID = passkeyConfig && passkeyConfig.dataset && String(passkeyConfig.dataset.passkeyRpId || '').toLowerCase();
|
||||
var currentHostname = String(window.location.hostname || '').toLowerCase();
|
||||
var passkeyHostnameMatches = passkeyRpID && (currentHostname === passkeyRpID || currentHostname.endsWith('.' + passkeyRpID));
|
||||
var deleteButtons = document.querySelectorAll('[data-passkey-delete]');
|
||||
|
||||
if (!window.isSecureContext || !navigator.credentials || !client || !passkeyHostnameMatches) {
|
||||
if (addForm) addForm.hidden = true;
|
||||
if (addTrigger) addTrigger.disabled = true;
|
||||
if (unavailable) unavailable.hidden = false;
|
||||
deleteButtons.forEach(function (button) { button.disabled = true; });
|
||||
return;
|
||||
}
|
||||
|
||||
function showError(error) {
|
||||
if (!feedback) return;
|
||||
feedback.textContent = error && error.message || 'The passkey request could not be completed.';
|
||||
feedback.hidden = false;
|
||||
}
|
||||
|
||||
if (addForm) {
|
||||
var setupModal = document.querySelector('#passkey-setup-modal');
|
||||
if (setupModal) {
|
||||
setupModal.addEventListener('shown.bs.modal', function () {
|
||||
addForm.elements.name.focus();
|
||||
});
|
||||
}
|
||||
|
||||
addForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
var button = addForm.querySelector('button[type="submit"]');
|
||||
button.disabled = true;
|
||||
if (feedback) feedback.hidden = true;
|
||||
if (progress) progress.hidden = false;
|
||||
try {
|
||||
var name = addForm.elements.name.value.trim();
|
||||
var result = await client.passkey.addPasskey(name ? { name: name } : {});
|
||||
if (result.error) throw new Error(result.error.message || 'Unable to add this passkey.');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
if (progress) progress.hidden = true;
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteButtons.forEach(function (button) {
|
||||
button.addEventListener('click', async function () {
|
||||
if (!window.confirm('Remove this passkey?')) return;
|
||||
button.disabled = true;
|
||||
if (feedback) feedback.hidden = true;
|
||||
try {
|
||||
var result = await client.passkey.deletePasskey({ id: button.dataset.passkeyDelete });
|
||||
if (result.error) throw new Error(result.error.message || 'Unable to remove this passkey.');
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,146 @@
|
||||
(function () {
|
||||
function getFeedback(form, selector) {
|
||||
return form.querySelector(selector);
|
||||
}
|
||||
|
||||
function showError(target, message) {
|
||||
if (!target) return;
|
||||
target.textContent = message;
|
||||
target.hidden = false;
|
||||
}
|
||||
|
||||
async function request(endpoint, body) {
|
||||
const response = await fetch('/api/auth/two-factor/' + endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body || {})
|
||||
});
|
||||
const data = await response.json().catch(function () { return {}; });
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.code || 'Two-factor request failed.');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function renderBackupCodes(container, codes) {
|
||||
const list = container && container.querySelector('[data-two-factor-codes-list]');
|
||||
if (!container || !list) return;
|
||||
list.replaceChildren();
|
||||
(Array.isArray(codes) ? codes : []).forEach(function (code) {
|
||||
const item = document.createElement('li');
|
||||
item.textContent = code;
|
||||
list.appendChild(item);
|
||||
});
|
||||
container.hidden = false;
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-two-factor-download]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
const container = button.closest('[data-two-factor-codes]');
|
||||
const list = container && container.querySelector('[data-two-factor-codes-list]');
|
||||
const codes = list ? Array.from(list.querySelectorAll('li')).map(function (item) {
|
||||
return item.textContent.trim();
|
||||
}).filter(Boolean) : [];
|
||||
if (!codes.length) return;
|
||||
const file = new Blob(['Pulse Signage recovery codes\n\n' + codes.join('\n') + '\n'], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(file);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'pulse-signage-recovery-codes.txt';
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
});
|
||||
|
||||
const enableForm = document.querySelector('[data-two-factor-enable]');
|
||||
if (enableForm) {
|
||||
const setupPanel = document.querySelector('[data-two-factor-setup]');
|
||||
const feedback = getFeedback(enableForm, '[data-two-factor-enable-feedback]');
|
||||
enableForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
const button = enableForm.querySelector('button[type="submit"]');
|
||||
const password = enableForm.elements.password.value;
|
||||
feedback.hidden = true;
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await request('enable', { password: password, method: 'totp', issuer: 'Pulse Signage' });
|
||||
if (result.method !== 'totp' || !result.totpURI) throw new Error('Authenticator setup could not be started.');
|
||||
const qrContainer = document.querySelector('[data-two-factor-qr]');
|
||||
qrContainer.replaceChildren();
|
||||
const qrCode = new window.QRCodeStyling({
|
||||
width: 256,
|
||||
height: 256,
|
||||
type: 'svg',
|
||||
data: result.totpURI,
|
||||
dotsOptions: { color: '#17212b', type: 'rounded' },
|
||||
backgroundOptions: { color: '#ffffff' },
|
||||
cornersSquareOptions: { color: '#17212b', type: 'extra-rounded' }
|
||||
});
|
||||
qrCode.append(qrContainer);
|
||||
const secret = new URL(result.totpURI).searchParams.get('secret');
|
||||
document.querySelector('[data-two-factor-secret]').textContent = secret || '';
|
||||
renderBackupCodes(document.querySelector('[data-two-factor-codes]'), result.backupCodes);
|
||||
setupPanel.hidden = false;
|
||||
enableForm.hidden = true;
|
||||
} catch (error) {
|
||||
showError(feedback, error.message);
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
const verifyForm = document.querySelector('[data-two-factor-verify]');
|
||||
const verifyFeedback = getFeedback(verifyForm, '[data-two-factor-verify-feedback]');
|
||||
verifyForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
const button = verifyForm.querySelector('button[type="submit"]');
|
||||
verifyFeedback.hidden = true;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await request('verify-totp', { code: verifyForm.elements.code.value.trim() });
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
showError(verifyFeedback, error.message);
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const disableForm = document.querySelector('[data-two-factor-disable]');
|
||||
if (disableForm) {
|
||||
const feedback = getFeedback(disableForm, '[data-two-factor-disable-feedback]');
|
||||
disableForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
const button = disableForm.querySelector('button[type="submit"]');
|
||||
feedback.hidden = true;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await request('disable', { password: disableForm.elements.password.value });
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
showError(feedback, error.message);
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const recoveryForm = document.querySelector('[data-two-factor-regenerate-codes]');
|
||||
if (recoveryForm) {
|
||||
const feedback = document.querySelector('[data-two-factor-codes-feedback]');
|
||||
recoveryForm.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
const button = recoveryForm.querySelector('button[type="submit"]');
|
||||
feedback.hidden = true;
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await request('generate-backup-codes', { password: recoveryForm.elements.password.value });
|
||||
renderBackupCodes(document.querySelector('[data-two-factor-codes]'), result.backupCodes);
|
||||
recoveryForm.elements.password.value = '';
|
||||
} catch (error) {
|
||||
showError(feedback, error.message);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,137 @@
|
||||
(function () {
|
||||
var form = document.querySelector('[data-better-auth-login]');
|
||||
if (!form) return;
|
||||
|
||||
var feedback = form.querySelector('[data-login-feedback]');
|
||||
var submitButton = form.querySelector('[data-login-submit]');
|
||||
var credentialsPanel = form.querySelector('[data-login-credentials]');
|
||||
var rememberPanel = form.querySelector('[data-login-remember]');
|
||||
var twoFactorPanel = form.querySelector('[data-login-two-factor]');
|
||||
var codeInput = form.elements.two_factor_code;
|
||||
var codeLabel = form.querySelector('[data-two-factor-code-label]');
|
||||
var toggleMethodButton = form.querySelector('[data-two-factor-toggle-method]');
|
||||
var backButton = form.querySelector('[data-two-factor-back]');
|
||||
var passkeyPanel = form.querySelector('[data-passkey-login-panel]');
|
||||
var passkeyButton = form.querySelector('[data-passkey-login]');
|
||||
var passkeyUnavailable = form.querySelector('[data-passkey-unavailable]');
|
||||
var passkeyRpID = passkeyPanel && passkeyPanel.dataset && String(passkeyPanel.dataset.passkeyRpId || '').toLowerCase();
|
||||
var currentHostname = String(window.location.hostname || '').toLowerCase();
|
||||
var passkeyHostnameMatches = passkeyRpID && (currentHostname === passkeyRpID || currentHostname.endsWith('.' + passkeyRpID));
|
||||
var challengeMethods = [];
|
||||
var challengeMode = 'totp';
|
||||
|
||||
function showChallenge(methods) {
|
||||
challengeMethods = Array.isArray(methods) ? methods : [];
|
||||
challengeMode = 'totp';
|
||||
credentialsPanel.hidden = true;
|
||||
rememberPanel.hidden = true;
|
||||
twoFactorPanel.hidden = false;
|
||||
if (passkeyPanel) passkeyPanel.hidden = true;
|
||||
codeInput.disabled = false;
|
||||
form.elements.trust_device.disabled = false;
|
||||
codeInput.value = '';
|
||||
codeLabel.textContent = 'Authenticator code';
|
||||
codeInput.inputMode = 'numeric';
|
||||
toggleMethodButton.hidden = false;
|
||||
toggleMethodButton.textContent = 'Use recovery code';
|
||||
submitButton.textContent = 'Verify code';
|
||||
submitButton.disabled = false;
|
||||
codeInput.focus();
|
||||
}
|
||||
|
||||
function resetChallenge() {
|
||||
challengeMethods = [];
|
||||
challengeMode = 'totp';
|
||||
credentialsPanel.hidden = false;
|
||||
rememberPanel.hidden = false;
|
||||
twoFactorPanel.hidden = true;
|
||||
if (passkeyPanel) passkeyPanel.hidden = false;
|
||||
codeInput.disabled = true;
|
||||
form.elements.trust_device.disabled = true;
|
||||
form.elements.trust_device.checked = false;
|
||||
codeInput.value = '';
|
||||
toggleMethodButton.textContent = 'Use recovery code';
|
||||
submitButton.textContent = 'Sign in';
|
||||
feedback.hidden = true;
|
||||
}
|
||||
|
||||
toggleMethodButton.addEventListener('click', function () {
|
||||
challengeMode = challengeMode === 'totp' ? 'backup' : 'totp';
|
||||
codeLabel.textContent = challengeMode === 'backup' ? 'Recovery code' : 'Authenticator code';
|
||||
codeInput.inputMode = challengeMode === 'backup' ? 'text' : 'numeric';
|
||||
codeInput.autocomplete = challengeMode === 'backup' ? 'off' : 'one-time-code';
|
||||
codeInput.value = '';
|
||||
toggleMethodButton.textContent = challengeMode === 'backup' ? 'Use authenticator code' : 'Use recovery code';
|
||||
codeInput.focus();
|
||||
});
|
||||
|
||||
backButton.addEventListener('click', resetChallenge);
|
||||
|
||||
if (passkeyButton) {
|
||||
if (!window.isSecureContext || !navigator.credentials || !window.pulsePasskeyAuth || !passkeyHostnameMatches) {
|
||||
passkeyButton.disabled = true;
|
||||
if (passkeyUnavailable) passkeyUnavailable.hidden = false;
|
||||
} else {
|
||||
passkeyButton.addEventListener('click', async function () {
|
||||
feedback.hidden = true;
|
||||
passkeyButton.disabled = true;
|
||||
try {
|
||||
var result = await window.pulsePasskeyAuth.signIn.passkey({ autoFill: false });
|
||||
if (result.error) throw new Error(result.error.message || 'Unable to sign in with a passkey.');
|
||||
var returnTo = new URLSearchParams(window.location.search).get('returnTo') || '/';
|
||||
window.location.assign(returnTo.charAt(0) === '/' && returnTo.charAt(1) !== '/' ? returnTo : '/');
|
||||
} catch (error) {
|
||||
feedback.textContent = error.message || 'Unable to sign in with a passkey.';
|
||||
feedback.hidden = false;
|
||||
passkeyButton.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
var initialMessage = document.querySelector('[data-login-initial-message]');
|
||||
if (initialMessage) initialMessage.remove();
|
||||
feedback.hidden = true;
|
||||
submitButton.disabled = true;
|
||||
|
||||
try {
|
||||
var inChallenge = challengeMethods.length > 0;
|
||||
var identity = inChallenge ? '' : form.elements.username.value.trim();
|
||||
var useEmail = identity.indexOf('@') !== -1;
|
||||
var endpoint = inChallenge
|
||||
? '/api/auth/two-factor/' + (challengeMode === 'backup' ? 'verify-backup-code' : 'verify-totp')
|
||||
: form.action;
|
||||
var payload = inChallenge
|
||||
? { code: codeInput.value.trim(), trustDevice: form.elements.trust_device.checked }
|
||||
: {
|
||||
username: useEmail ? undefined : identity,
|
||||
email: useEmail ? identity : undefined,
|
||||
password: form.elements.password.value,
|
||||
rememberMe: form.elements.remember_me.checked
|
||||
};
|
||||
var response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
var result = await response.json().catch(function () { return {}; });
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || (inChallenge ? 'Unable to verify the two-factor code.' : 'Unable to sign in. Check your username and password.'));
|
||||
}
|
||||
if (!inChallenge && result.twoFactorRedirect) {
|
||||
showChallenge(result.twoFactorMethods);
|
||||
return;
|
||||
}
|
||||
|
||||
var returnTo = new URLSearchParams(window.location.search).get('returnTo') || '/';
|
||||
window.location.assign(returnTo.charAt(0) === '/' && returnTo.charAt(1) !== '/' ? returnTo : '/');
|
||||
} catch (error) {
|
||||
feedback.textContent = error.message || 'Unable to sign in. Check your username and password.';
|
||||
feedback.hidden = false;
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createAuthClient } from 'better-auth/client';
|
||||
import { passkeyClient } from '@better-auth/passkey/client';
|
||||
|
||||
window.pulsePasskeyAuth = createAuthClient({
|
||||
plugins: [passkeyClient()]
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
(()=>{var le=Symbol.for("better-auth:broadcast-channel"),ft=()=>Math.floor(Date.now()/1e3),dt=class{listeners=new Set;name;constructor(e="better-auth.message"){this.name=e}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}post(e){if(!(typeof window>"u"))try{localStorage.setItem(this.name,JSON.stringify({...e,timestamp:ft()}))}catch{}}setup(){if(typeof window>"u"||typeof window.addEventListener>"u")return()=>{};let e=t=>{if(t.key!==this.name)return;let r=JSON.parse(t.newValue??"{}");r?.event!=="session"||!r?.data||this.listeners.forEach(n=>n(r))};return window.addEventListener("storage",e),()=>{window.removeEventListener("storage",e)}}};function J(e="better-auth.message"){return globalThis[le]||(globalThis[le]=new dt(e)),globalThis[le]}var O=[],H=0,C=null,ce=4,ht=globalThis.nanostoresGlobal||={epoch:0},pt=()=>{let e,t;for(;H<O.length;){t=H,H+=ce;let r=C?.get(O[t]);if(r){if(!r.has(O[t+1]))continue;r.clear()}try{O[t](O[t+1].value,O[t+2],O[t+3])}catch(n){e=n}}if(O.length=H=0,e)throw e};var D=e=>{let t=[],r={eq:Object.is,get(){return r.lc||r.listen(()=>{})(),r.value},init:e,lc:0,listen(n){return r.lc=t.push(n),()=>{C?.get(n)?.delete(r);for(let s=H;s<O.length;)O[s]===n&&O[s+1]===r?O.splice(s,ce):s+=ce;let o=t.indexOf(n);~o&&(t.splice(o,1),--r.lc||r.off())}},notify(n,o){ht.epoch++;let s=!O.length&&!C;for(let l of t){if(C){let a=C.get(l);if(a||C.set(l,a=new Set),a.has(r))continue;a.add(r)}O.push(l,r,n,C?void 0:o)}s&&pt()},off(){},set(n){let o=r.value;r.eq(o,n)||(r.value=n,r.notify(o))},subscribe(n){let o=r.listen(n);return n(r.value),o},value:e};return r};var mt=2;var Et=5,$=6,z=10,_e=(e,t,r,n)=>(e.events=e.events||{},e.events[r+z]||(e.events[r+z]=n(o=>{e.events[r].reduceRight((s,l)=>(l(s),s),{shared:{},...o})})),e.events[r]=e.events[r]||[],e.events[r].push(t),()=>{let o=e.events[r],s=o.indexOf(t);~s&&(o.splice(s,1),o.length||(e.events[r+z](),delete e.events[r+z]))});var fe=(e,t)=>_e(e,t,mt,r=>{let n=e.set,o=e.setKey;return e.setKey&&(e.setKey=(s,l)=>{let a;if(r({abort:()=>{a=!0},changed:s,newValue:{...e.value,[s]:l}}),!a)return o(s,l)}),e.set=s=>{let l;if(r({abort:()=>{l=!0},newValue:s}),!l)return n(s)},()=>{e.set=n,e.setKey=o}});var Q=1e3,j=(e,t)=>_e(e,n=>{let o=t(n);o&&e.events[$].push(o)},Et,n=>{let o=e.listen;e.listen=(...l)=>(!e.lc&&!e.active&&(e.active=!0,n()),o(...l));let s=e.off;return e.events[$]=[],e.off=()=>{s(),setTimeout(()=>{if(e.active&&!e.lc){e.active=!1;for(let l of e.events[$])l();e.events[$]=[]}},Q)},()=>{e.listen=o,e.off=s}});function Pe(e){if(typeof e!="object"||e===null)return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function L(e,t){if(e===t)return!0;if(Array.isArray(e)&&Array.isArray(t)){if(e.length!==t.length)return!1;for(let r=0;r<e.length;r++)if(!L(e[r],t[r]))return!1;return!0}if(Pe(e)&&Pe(t)){let r=Object.keys(e),n=Object.keys(t);if(r.length!==n.length)return!1;for(let o of r)if(!(o in t)||!L(e[o],t[o]))return!1;return!0}return!1}function X(e,t){return fe(e,({newValue:r,abort:n})=>{t(e.value,r)&&n()})}var de=Symbol.for("better-auth:focus-manager"),yt=class{listeners=new Set;subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}setFocused(e){this.listeners.forEach(t=>t(e))}setup(){if(typeof window>"u"||typeof document>"u"||typeof window.addEventListener>"u")return()=>{};let e=()=>{document.visibilityState==="visible"&&this.setFocused(!0)};return document.addEventListener("visibilitychange",e,!1),()=>{document.removeEventListener("visibilitychange",e,!1)}}};function he(){return globalThis[de]||(globalThis[de]=new yt),globalThis[de]}var pe=Symbol.for("better-auth:online-manager"),gt=class{listeners=new Set;isOnline=typeof navigator<"u"?navigator.onLine:!0;subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}setOnline(e){this.isOnline=e,this.listeners.forEach(t=>t(e))}setup(){if(typeof window>"u"||typeof window.addEventListener>"u")return()=>{};let e=()=>this.setOnline(!0),t=()=>this.setOnline(!1);return window.addEventListener("online",e,!1),window.addEventListener("offline",t,!1),()=>{window.removeEventListener("online",e,!1),window.removeEventListener("offline",t,!1)}}};function Z(){return globalThis[pe]||(globalThis[pe]=new gt),globalThis[pe]}var Rt={proto:/"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/,constructor:/"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/,protoShort:/"__proto__"\s*:/,constructorShort:/"constructor"\s*:/},bt=/^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/,Ue={true:!0,false:!1,null:null,undefined:void 0,nan:NaN,infinity:Number.POSITIVE_INFINITY,"-infinity":Number.NEGATIVE_INFINITY},vt=/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,7}))?(?:Z|([+-])(\d{2}):(\d{2}))$/;function Tt(e){return e instanceof Date&&!isNaN(e.getTime())}function St(e){let t=vt.exec(e);iLine truncated
|
||||
at `);return t.length<=1?e:(t.splice(1,1),t.join(`
|
||||
at `))}function Ct(e,t){class r extends e{#e;constructor(...o){if(Lt()){let l=Error.stackTraceLimit;Error.stackTraceLimit=0,super(...o),Error.stackTraceLimit=l}else super(...o);let s=new Error().stack;s&&(this.#e=Nt(s.replace(/^Error/,this.name)))}get errorStack(){return this.#e}}return Object.defineProperty(r.prototype,"constructor",{get(){return t},enumerable:!1,configurable:!0}),r}var Dt={OK:200,CREATED:201,ACCEPTED:202,NO_CONTENT:204,MULTIPLE_CHOICES:300,MOVED_PERMANENTLY:301,FOUND:302,SEE_OTHER:303,NOT_MODIFIED:304,TEMPORARY_REDIRECT:307,BAD_REQUEST:400,UNAUTHORIZED:401,PAYMENT_REQUIRED:402,FORBIDDEN:403,NOT_FOUND:404,METHOD_NOT_ALLOWED:405,NOT_ACCEPTABLE:406,PROXY_AUTHENTICATION_REQUIRED:407,REQUEST_TIMEOUT:408,CONFLICT:409,GONE:410,LENGTH_REQUIRED:411,PRECONDITION_FAILED:412,PAYLOAD_TOO_LARGE:413,URI_TOO_LONG:414,UNSUPPORTED_MEDIA_TYPE:415,RANGE_NOT_SATISFIABLE:416,EXPECTATION_FAILED:417,"I'M_A_TEAPOT":418,MISDIRECTED_REQUEST:421,UNPROCESSABLE_ENTITY:422,LOCKED:423,FAILED_DEPENDENCY:424,TOO_EARLY:425,UPGRADE_REQUIRED:426,PRECONDITION_REQUIRED:428,TOO_MANY_REQUESTS:429,REQUEST_HEADER_FIELDS_TOO_LARGE:431,UNAVAILABLE_FOR_LEGAL_REASONS:451,INTERNAL_SERVER_ERROR:500,NOT_IMPLEMENTED:501,BAD_GATEWAY:502,SERVICE_UNAVAILABLE:503,GATEWAY_TIMEOUT:504,HTTP_VERSION_NOT_SUPPORTED:505,VARIANT_ALSO_NEGOTIATES:506,INSUFFICIENT_STORAGE:507,LOOP_DETECTED:508,NOT_EXTENDED:510,NETWORK_AUTHENTICATION_REQUIRED:511},xt=class extends Error{status;body;headers;statusCode;constructor(e="INTERNAL_SERVER_ERROR",t=void 0,r={},n=typeof e=="number"?e:Dt[e]){if(super(t?.message,t&&"cause"in t?{cause:t.cause}:void 0),this.status=e,this.body=t,this.headers=r,this.statusCode=n,this.name="APIError",this.status=e,this.headers=r,this.statusCode=n,t&&"cause"in t){let{cause:o,...s}=t;this.body=Object.defineProperty(s,"cause",{value:o,writable:!0,configurable:!0});return}this.body=t}};var kt=Ct(xt,Error);var F=class extends Error{constructor(e,t){super(e,t),this.name="BetterAuthError",this.message=e,this.stack=""}};var Mt=47;function De(e){let t=e.length;for(;t>0&&e.charCodeAt(t-1)===Mt;)t--;return t===e.length?e:e.slice(0,t)}function Bt(e){try{return(De(new URL(e).pathname)||"/")!=="/"}catch{throw new F(`Invalid base URL: ${e}. Please provide a valid base URL.`)}}function qt(e){try{let t=new URL(e);if(t.protocol!=="http:"&&t.protocol!=="https:")throw new F(`Invalid base URL: ${e}. URL must include 'http://' or 'https://'`)}catch(t){throw t instanceof F?t:new F(`Invalid base URL: ${e}. Please provide a valid base URL.`,{cause:t})}}function W(e,t="/api/auth"){if(qt(e),Bt(e))return e;let r=De(e);return!t||t==="/"?r:(t=t.startsWith("/")?t:`/${t}`,`${r}${t}`)}function Ce(e,t){return!e||e.trim()===""?!1:t==="proto"?e==="http"||e==="https":t==="host"?[/\.\./,/\0/,/[\s]/,/^[.]/,/[<>'"]/,/javascript:/i,/file:/i,/data:/i].some(r=>r.test(e))?!1:/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]{1,5})?$/.test(e)||/^(\d{1,3}\.){3}\d{1,3}(:[0-9]{1,5})?$/.test(e)||/^\[[0-9a-fA-F:]+\](:[0-9]{1,5})?$/.test(e)||/^localhost(:[0-9]{1,5})?$/i.test(e):!1}function xe(e,t,r,n,o){if(e)return W(e,t);if(n!==!1){let a=I.BETTER_AUTH_URL||I.NEXT_PUBLIC_BETTER_AUTH_URL||I.PUBLIC_BETTER_AUTH_URL||I.NUXT_PUBLIC_BETTER_AUTH_URL||I.NUXT_PUBLIC_AUTH_URL||(I.BASE_URL!=="/"?I.BASE_URL:void 0);if(a)return W(a,t)}let s=r?.headers.get("x-forwarded-host"),l=r?.headers.get("x-forwarded-proto");if(s&&l&&o&&Ce(l,"proto")&&Ce(s,"host"))try{return W(`${l}://${s}`,t)}catch{}if(r){let a=jt(r.url);if(!a)throw new F("Could not get origin from request. Please provide a valid base URL.");return W(a,t)}if(typeof window<"u"&&window.location)return W(window.location.origin,t)}function jt(e){try{let t=new URL(e);return t.origin==="null"?null:t.origin}catch{return null}}var Ft=["javascript:","data:","vbscript:"];function ke(e){let t;try{t=new URL(e)}catch{return!0}return!Ft.includes(t.protocol)}var Me={id:"redirect",name:"Redirect",hooks:{onSuccess(e){if(e.data?.url&&e.data?.redirect&&ke(e.data.url)&&typeof window<"u"&&window.location&&window.location)try{window.location.href=e.data.url}catch{}}}};var Ht=()=>typeof window>"u",Vt=Q;function qe(e,t){if(typeof window>"u")return;let r=e.get();r.data!==null||t===null||e.set({...r,data:t,error:null,isPending:!1})}function Be(e){return typeof e=="object"&&e!==null&&"data"in e&&"error"in e?e:{data:e,error:null}}function Wt(e){return!e||e.session===null&&e.user===null?null:e}function Gt(e,t){return L(e.data,t.data)&&e.error===t.error&&e.isPending===t.isPending&&e.isRefetching===t.isRefetching&&e.refetch===t.refetch}function je(e,t){let r=D(!1),n,o=0,s=0;r.listen(()=>{s++,o=0});let l=m=>u(m),a=ee({data:null,error:null,isPending:!0,isRefetching:!1,refetch:l});X(a,Gt);let i=async(m,E)=>{let f=a.value;if(a.set({...f,isPending:f.data===null,isRefetching:!0,error:null,refetch:l}),m.aborted)return"aborted";try{let g=await e("/get-session",{method:"GET",query:E?.query,signal:m});if(m.aborted)return"aborted";l
Line truncated
|
||||
`,t)}function it(){if(!M())return Oe.stubThis(new Promise(t=>t(!1)));let e=globalThis.PublicKeyCredential;return e?.isConditionalMediationAvailable===void 0?Oe.stubThis(new Promise(t=>t(!1))):Oe.stubThis(e.isConditionalMediationAvailable())}var Oe={stubThis:e=>e};function at({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new v({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new v({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){let n=globalThis.location.hostname;if(ne(n)){if(r.rpId!==n)return new v({message:`The RP ID "${r.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new v({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new v({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function ut(e){!e.optionsJSON&&e.challenge&&(console.warn("startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useBrowserAutofill:r=!1,verifyBrowserAutofillInput:n=!0}=e;if(!M())throw new Error("WebAuthn is not supported in this browser");let o;t.allowCredentials?.length!==0&&(o=t.allowCredentials?.map(re));let s={...t,challenge:k(t.challenge),allowCredentials:o},l={};if(r){if(!await it())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&n)throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');l.mediation="conditional",s.allowCredentials=[]}l.publicKey=s,l.signal=oe.createNewAbortSignal();let a;try{a=await navigator.credentials.get(l)}catch(m){throw at({error:m,options:l})}if(!a)throw new Error("Authentication was not completed");let{id:i,rawId:c,response:u,type:d}=a,p;return u.userHandle&&(p=P(u.userHandle)),{id:i,rawId:P(c),response:{authenticatorData:P(u.authenticatorData),clientDataJSON:P(u.clientDataJSON),signature:P(u.signature),userHandle:p},type:d,clientExtensionResults:a.getClientExtensionResults(),authenticatorAttachment:se(a.authenticatorAttachment)}}var Lr=(e,{$listPasskeys:t,$store:r})=>({signIn:{passkey:async(s,l)=>{let a=await e("/passkey/generate-authenticate-options",{method:"GET",throw:!1});if(!a.data)return a;let i=a.data.extensions||s?.extensions?{...a.data.extensions||{},...s?.extensions||{}}:void 0,c;try{c=await ut({optionsJSON:{...a.data,extensions:i},useBrowserAutofill:s?.autoFill})}catch(u){return{data:null,error:{code:u instanceof v?u.code:"AUTH_CANCELLED",message:x.AUTH_CANCELLED.message,status:400,statusText:"BAD_REQUEST"}}}try{let{clientExtensionResults:u,...d}=c,p=await e("/passkey/verify-authentication",{body:{response:d},...s?.fetchOptions,...l,method:"POST",throw:!1});return t.set(Math.random()),r.notify("$sessionSignal"),s?.returnWebAuthnResponse?{...p,webauthn:{response:c,clientExtensionResults:u}}:p}catch(u){return console.error("[Better Auth] Error verifying passkey",u),{data:null,error:{code:"AUTH_CANCELLED",message:x.AUTH_CANCELLED.message,status:400,statusText:"BAD_REQUEST"}}}}},passkey:{addPasskey:async(s,l)=>{let a=await e("/passkey/generate-register-options",{method:"GET",query:{...s?.authenticatorAttachment&&{authenticatorAttachment:s.authenticatorAttachment},...s?.name&&{name:s.name},...s?.context&&{context:s.context}},throw:!1});if(!a.data)return a;try{let i=a.data.extensions||s?.extensions?{...a.data.extensions||{},...s?.extensions||{}}:void 0,c=await st({optionsJSON:{...a.data,extensions:i},useAutoRegister:s?.useAutoRegister}),{clientExtensionResults:u,...d}=c,p=await e("/passkey/verify-registration",{...s?.fetchOptions,...l,body:{response:d,name:s?.name,...s?.createSession&&{createSession:!0}},method:"POST",throw:!1});return p.data?(t.set(Math.random()),p.data.session&&r.notify("$sessionSignal"),s?.returnWebAuthnResponse?{...p,webauthn:{response:c,clientExtensionResults:u}}:p):p}catch(i){return i instanceof v?i.code==="ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED"?{data:null,error:{code:i.code,message:x.PREVIOUSLY_REGISTERED.message,status:400,statusText:"BAD_REQUEST"}}:i.code==="ERROR_CEREMONY_ABORTED"?{data:null,error:{code:i.code,message:x.REGISTRATION_CANCELLED.message,status:400,statusText:"BAD_REQUEST"}}:{data:null,error:{code:i.code,message:i.message,status:400,statusText:"BAD_REQUEST"}}:{data:null,error:{code:"UN
Line truncated
|
||||
@@ -390,20 +390,13 @@
|
||||
username: previewUser ? previewUser.getAttribute('data-username') : '',
|
||||
display_name: previewUser ? previewUser.getAttribute('data-display-name') : '',
|
||||
email: previewUser ? previewUser.getAttribute('data-email') : '',
|
||||
verification_expiry: previewUser ? previewUser.getAttribute('data-verification-expiry') : '30',
|
||||
reset_expiry: previewUser ? previewUser.getAttribute('data-reset-expiry') : '30',
|
||||
invitation_expiry: previewUser ? previewUser.getAttribute('data-invitation-expiry') : '24'
|
||||
otp_expiry: previewUser ? previewUser.getAttribute('data-otp-expiry') : '30'
|
||||
};
|
||||
document.querySelectorAll('[data-email-template-editor]').forEach(function (editor) {
|
||||
var subject = editor.querySelector('[data-email-template-subject]');
|
||||
var body = editor.querySelector('[data-email-template-body]');
|
||||
var alignment = editor.querySelector('[data-email-template-alignment]');
|
||||
var buttonText = editor.querySelector('[data-email-template-button-text]');
|
||||
var preview = editor.querySelector('[data-email-template-preview]');
|
||||
if (!subject || !body || !preview) return;
|
||||
var isVerificationTemplate = Boolean(editor.querySelector('[name="verification_subject"]'));
|
||||
var isResetTemplate = Boolean(editor.querySelector('[name="reset_subject"]'));
|
||||
var isInvitationTemplate = Boolean(editor.querySelector('[name="invitation_subject"]'));
|
||||
|
||||
function render() {
|
||||
preview.innerHTML = '';
|
||||
@@ -418,45 +411,16 @@
|
||||
username: signedInUser.username || 'username',
|
||||
display_name: signedInUser.display_name || signedInUser.username || 'Your name',
|
||||
email: signedInUser.email || 'your-email@example.com',
|
||||
expiry_time: isInvitationTemplate ? signedInUser.invitation_expiry + ' hours' : (isResetTemplate ? signedInUser.reset_expiry : signedInUser.verification_expiry) + ' minutes'
|
||||
otp: '123456',
|
||||
expiry_time: signedInUser.otp_expiry + ' minutes'
|
||||
};
|
||||
var button = document.createElement('div');
|
||||
button.className = 'email-template-preview__button';
|
||||
var buttonAlignment = alignment ? alignment.value : 'center';
|
||||
var buttonWrap = document.createElement('div');
|
||||
buttonWrap.className = 'email-template-preview__button-wrap';
|
||||
buttonWrap.style.textAlign = buttonAlignment;
|
||||
buttonWrap.appendChild(button);
|
||||
var previewActionPlaceholder = '[[url]]';
|
||||
button.textContent = buttonText && buttonText.value ? buttonText.value : (isVerificationTemplate ? 'Verify email address' : (isInvitationTemplate ? 'Accept invitation' : 'Reset password'));
|
||||
var previewOrigin = window.location.origin || 'https://pulse-signage.example';
|
||||
var previewVerificationUrl = previewOrigin + '/verify-email?token=preview-token';
|
||||
var previewResetUrl = previewOrigin + '/reset-password?token=preview-token';
|
||||
var previewActionUrl = isVerificationTemplate ? previewVerificationUrl : (isInvitationTemplate ? previewOrigin + '/accept-invite?token=preview-token' : previewResetUrl);
|
||||
var hasExplicitButton = body.value.indexOf('[[action_button]]') !== -1;
|
||||
body.value.split(/\r?\n(?:[ \t]*\r?\n)+/).forEach(function (sourceParagraph) {
|
||||
var line = sourceParagraph.replace(/\[\[(username|display_name|email|expiry_time)\]\]/g, function (_match, key) { return sampleValues[key]; }).replace(previewActionPlaceholder, previewActionUrl);
|
||||
var hasButtonToken = sourceParagraph.indexOf('[[action_button]]') !== -1;
|
||||
var hasUrlToken = !hasExplicitButton && sourceParagraph.indexOf(previewActionPlaceholder) !== -1;
|
||||
if (hasButtonToken) {
|
||||
var buttonParts = line.split('[[action_button]]');
|
||||
if (buttonParts[0]) appendPreviewParagraph(buttonParts[0]);
|
||||
message.appendChild(buttonWrap);
|
||||
if (buttonParts[1]) appendPreviewParagraph(buttonParts[1]);
|
||||
return;
|
||||
}
|
||||
if (hasUrlToken) {
|
||||
var legacyParts = sourceParagraph.split(previewActionPlaceholder);
|
||||
if (legacyParts[0]) appendPreviewParagraph(legacyParts[0]);
|
||||
message.appendChild(buttonWrap);
|
||||
appendPreviewParagraph(previewActionUrl + legacyParts[1]);
|
||||
return;
|
||||
}
|
||||
var line = sourceParagraph.replace(/\[\[(username|display_name|email|expiry_time|otp)\]\]/g, function (_match, key) { return sampleValues[key]; });
|
||||
appendPreviewParagraph(line);
|
||||
});
|
||||
function appendPreviewParagraph(line) {
|
||||
var paragraph = document.createElement('p');
|
||||
paragraph.innerHTML = (line || '\u00a0').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\r?\n/g, '<br>').replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1">$1</a>').replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '<strong>$1</strong>').replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '<em>$1</em>').replace(/\[u\]([\s\S]*?)\[\/u\]/gi, '<u>$1</u>');
|
||||
paragraph.innerHTML = (line || '\u00a0').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\r?\n/g, '<br>').replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '<strong>$1</strong>').replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '<em>$1</em>').replace(/\[u\]([\s\S]*?)\[\/u\]/gi, '<u>$1</u>');
|
||||
message.appendChild(paragraph);
|
||||
}
|
||||
preview.appendChild(brand);
|
||||
@@ -466,8 +430,6 @@
|
||||
|
||||
subject.addEventListener('input', render);
|
||||
body.addEventListener('input', render);
|
||||
if (alignment) alignment.addEventListener('change', render);
|
||||
if (buttonText) buttonText.addEventListener('input', render);
|
||||
editor.querySelectorAll('[data-email-format]').forEach(function (formatButton) {
|
||||
formatButton.addEventListener('click', function () {
|
||||
var tag = formatButton.getAttribute('data-email-format');
|
||||
|
||||
Reference in new issue
Block a user