web-mojo
Version:
WEB-MOJO - A lightweight JavaScript framework for building data-driven web applications
1 lines • 69.9 kB
JavaScript
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./chunks/WebApp-sKJf8j1s.js"),s=require("./chunks/TokenManager-gp1JKXNd.js"),t=require("./chunks/Page-D5kSAjt8.js");class AuthManager{constructor(e,t={}){this.app=e,this.config={autoRefresh:!0,refreshThreshold:5,plugins:{},...t},this.tokenManager=new s.TokenManager,this.isAuthenticated=!1,this.user=null,this.refreshTimer=null,this.plugins=/* @__PURE__ */new Map,this.initialize()}initialize(){this.checkAuthState(),this.config.autoRefresh&&this.scheduleTokenRefresh(),this.app&&(this.app.auth=this)}checkAuthState(){if(this.tokenManager.isValid()){const e=this.tokenManager.getUserInfo();if(e)return this.setAuthState(e),!0}return this.clearAuthState(),!1}async login(e,s,t=!0){const a=await this.app.rest.POST("/api/login",{username:e,password:s});if(a.success&&a.data.status){const{access_token:e,refresh_token:s,user:n}=a.data.data;this.tokenManager.setTokens(e,s,t);const i=this.tokenManager.getUserInfo();return this.setAuthState({...n,...i}),this.config.autoRefresh&&this.scheduleTokenRefresh(),this.emit("login",this.user),{success:!0,user:this.user}}const n=a.data?.error||a.message||"Login failed. Please try again.";return this.emit("loginError",{message:n}),{success:!1,message:n}}async register(e){const s=await this.app.rest.POST("/api/register",e);if(s.success&&s.data.status){const{token:e,refreshToken:t,user:a}=s.data.data;this.tokenManager.setTokens(e,t,!0);const n=this.tokenManager.getUserInfo();return this.setAuthState({...a,...n}),this.config.autoRefresh&&this.scheduleTokenRefresh(),this.emit("register",this.user),{success:!0,user:this.user}}const t=s.data?.error||s.message||"Registration failed.";return this.emit("registerError",{message:t}),{success:!1,message:t}}async logout(){try{this.tokenManager.getToken()&&this.app.rest.POST("/api/auth/logout").catch(e=>{console.warn("Server logout failed, proceeding with local logout.",e)})}finally{this.clearAuthState(),this.emit("logout")}}async refreshToken(){const e=this.tokenManager.getRefreshToken();if(!e)return this.clearAuthState(),this.emit("tokenExpired"),!1;const s=await this.app.rest.POST("/api/auth/token/refresh",{refreshToken:e});if(s.success&&s.data.status){const{token:e,refreshToken:t}=s.data.data,a=!!localStorage.getItem(this.tokenManager.tokenKey);this.tokenManager.setTokens(e,t,a);const n=this.tokenManager.getUserInfo();return n&&(this.user={...this.user,...n}),this.scheduleTokenRefresh(),this.emit("tokenRefreshed"),!0}return console.error("Token refresh failed:",s.data?.error||s.message),this.clearAuthState(),this.emit("tokenExpired"),!1}setAuthState(e){this.isAuthenticated=!0,this.user=e,this.app?.setState&&this.app.setState("auth",{isAuthenticated:!0,user:e})}clearAuthState(){this.isAuthenticated=!1,this.user=null,this.tokenManager.clearTokens(),this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=null),this.app?.setState&&this.app.setState("auth",{isAuthenticated:!1,user:null})}scheduleTokenRefresh(){if(this.refreshTimer&&clearTimeout(this.refreshTimer),!this.tokenManager.isValid())return;if(this.tokenManager.isExpiringSoon(this.config.refreshThreshold))return void this.refreshToken();const e=this.tokenManager.getToken(),s=this.tokenManager.decode(e);if(s?.exp){const e=Math.floor(Date.now()/1e3),t=1e3*(s.exp-e-60*this.config.refreshThreshold);t>0&&(this.refreshTimer=setTimeout(()=>{this.refreshToken()},t))}}registerPlugin(e,s){this.plugins.set(e,s),s.initialize(this,this.app)}getPlugin(e){return this.plugins.get(e)||null}async forgotPassword(e,s="code"){const t=await this.app.rest.POST("/api/auth/forgot",{email:e,method:s});if(t.success&&t.data.status)return this.emit("forgotPasswordSuccess",{email:e,method:s}),{success:!0,message:t.data.data?.message};const a=t.data?.error||t.message||"Failed to process request.";return this.emit("forgotPasswordError",{message:a}),{success:!1,message:a}}async resetPasswordWithToken(e,s){const t={token:e,new_password:s},a=await this.app.rest.POST("/api/auth/password/reset/token",t);if(a.success&&a.data.status)return this.emit("resetPasswordSuccess"),{success:!0,message:a.data.data?.message};const n=a.data?.error||a.message||"Failed to reset password.";return this.emit("resetPasswordError",{message:n}),{success:!1,message:n}}async resetPasswordWithCode(e,s,t){const a={email:e,code:s,new_password:t},n=await this.app.rest.POST("/api/auth/password/reset/code",a);if(n.success&&n.data.status)return this.emit("resetPasswordSuccess"),{success:!0,message:n.data.data?.message};const i=n.data?.error||n.message||"Failed to reset password.";return this.emit("resetPasswordError",{message:i}),{success:!1,message:i}}getAuthHeader(){return this.tokenManager.getAuthHeader()}emit(e,s){this.app?.events?.emit&&this.app.events.emit(`auth:${e}`,s)}destroy(){this.refreshTimer&&clearTimeout(this.refreshTimer),this.plugins.forEach(e=>{e.destroy&&e.destroy()}),this.plugins.clear()}}class LoginPage extends t.Page{static pageName="login";static title="Login";static icon="bi-box-arrow-in-right";static route="/login";constructor(e={}){super({...e,pageName:LoginPage.pageName,route:e.route||LoginPage.route,pageIcon:LoginPage.icon,template:e.template}),this.authConfig=e.authConfig||{ui:{title:"My App",logoUrl:"/assets/logo.png",messages:{loginTitle:"Welcome Back",loginSubtitle:"Sign in to your account"}},features:{rememberMe:!1,forgotPassword:!0,registration:!1}}}async onInit(){await super.onInit(),this.data={username:"",password:"",rememberMe:!0,loginIcon:this.options.pageIcon,isLoading:!1,error:null,showPassword:!1,version:e.VERSION,passkeySupported:this.getApp().auth?.isPasskeySupported?.()||!1,...this.authConfig.ui,...this.authConfig.features}}async onEnter(){await super.onEnter(),document.title=`${LoginPage.title} - ${this.authConfig.ui.title}`;const e=this.getApp().auth;e?.isAuthenticated?this.getApp().navigate("/"):this.updateData({username:"",password:"",error:null,isLoading:!1})}async onAfterRender(){await super.onAfterRender();const e=this.element.querySelector("#loginUsername");e&&e.focus()}async onActionUpdateField(e,s){const t=s.dataset.field,a="checkbox"===s.type?s.checked:s.value;this.updateData({[t]:a,error:null})}async onActionTogglePassword(e){e.preventDefault(),this.updateData({showPassword:!this.data.showPassword});const s=this.element.querySelector("#loginPassword");s&&(s.type=this.data.showPassword?"text":"password")}async onActionLogin(e){e.preventDefault(),this.data.username=this.element.querySelector("#loginUsername")?.value||"",this.data.password=this.element.querySelector("#loginPassword")?.value||"",await this.updateData({error:null,isLoading:!0},!0);const s=this.getApp().auth;if(!s)return void(await this.updateData({error:"Authentication system not available",isLoading:!1},!0));if(!this.data.username||!this.data.password)return void(await this.updateData({error:"Please enter both username and password",isLoading:!1},!0));const t=await s.login(this.data.username,this.data.password,this.data.rememberMe);if(!t.success){await this.updateData({error:t.message,isLoading:!1},!0);const e=this.element.querySelector("#loginPassword");e&&(e.focus(),e.select())}}async onActionLoginWithPasskey(e){e.preventDefault();const s=this.getApp().auth;if(s?.isPasskeySupported?.()){this.updateData({error:null,isLoading:!0});try{(await s.loginWithPasskey()).success&&console.log("Passkey login successful")}catch(t){console.error("Passkey login error:",t),this.updateData({error:"Passkey authentication failed. Please try another method.",isLoading:!1})}}else this.getApp().showError("Passkey authentication is not supported")}async onActionRegister(e){e.preventDefault(),this.getApp().navigate("/register")}async onActionForgotPassword(e){e.preventDefault(),this.getApp().navigate("/forgot-password")}async onActionHandleKeyPress(e,s){if("Enter"===e.key)if(e.preventDefault(),"loginUsername"===s.id){const e=this.element.querySelector("#loginPassword");e&&e.focus()}else"loginPassword"===s.id&&await this.onActionLogin(e)}async getViewData(){return{...this.data}}}class RegisterPage extends t.Page{static pageName="auth-register";static title="Register";static icon="bi-person-plus";static route="register";constructor(e={}){super({...e,pageName:RegisterPage.pageName,route:e.route||RegisterPage.route,pageIcon:RegisterPage.icon,template:e.template}),this.authConfig=e.authConfig||{ui:{title:"My App",logoUrl:"/assets/logo.png",messages:{registerTitle:"Create Account",registerSubtitle:"Join us today"}},features:{registration:!0}}}async onInit(){await super.onInit(),this.data={...this.authConfig.ui,...this.authConfig.features,name:"",email:"",password:"",confirmPassword:"",acceptTerms:!1,isLoading:!1,error:null,showPassword:!1,showConfirmPassword:!1,passwordStrength:null,passwordMatch:!0}}async onEnter(){await super.onEnter(),document.title=`${RegisterPage.title} - ${this.authConfig.ui.title}`;const e=this.getApp().auth;e?.isAuthenticated?this.getApp().navigate("/"):this.updateData({name:"",email:"",password:"",confirmPassword:"",acceptTerms:!1,error:null,isLoading:!1,passwordStrength:null,passwordMatch:!0})}async onAfterRender(){await super.onAfterRender();const e=this.element.querySelector("#registerName");e&&e.focus()}async onActionUpdateField(e,s){const t=s.dataset.field,a="checkbox"===s.type?s.checked:s.value;this.updateData({[t]:a}),"password"===t&&this.checkPasswordStrength(a),"password"!==t&&"confirmPassword"!==t||this.checkPasswordMatch(),this.data.error&&this.updateData({error:null})}checkPasswordStrength(e){let s=null;s=0===e.length?null:e.length<6?"weak":e.length<8?"fair":/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(e)?"strong":/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(e)?"good":"fair",this.updateData({passwordStrength:s},!0)}checkPasswordMatch(){const e=!this.data.confirmPassword||this.data.password===this.data.confirmPassword;this.updateData({passwordMatch:e},!0)}async onActionTogglePassword(e,s){e.preventDefault();const t=s.dataset.passwordField;if("password"===t){this.updateData({showPassword:!this.data.showPassword});const e=this.element.querySelector("#registerPassword");e&&(e.type=this.data.showPassword?"text":"password")}else if("confirmPassword"===t){this.updateData({showConfirmPassword:!this.data.showConfirmPassword});const e=this.element.querySelector("#registerConfirmPassword");e&&(e.type=this.data.showConfirmPassword?"text":"password")}}async onActionRegister(e){if(e.preventDefault(),await this.updateData({error:null,isLoading:!0},!0),!(this.data.name&&this.data.email&&this.data.password&&this.data.confirmPassword))return void(await this.updateData({error:"Please fill in all required fields",isLoading:!1},!0));if(this.data.name.trim().length<2)return void(await this.updateData({error:"Name must be at least 2 characters long",isLoading:!1},!0));if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.data.email))return void(await this.updateData({error:"Please enter a valid email address",isLoading:!1},!0));if(this.data.password.length<6)return void(await this.updateData({error:"Password must be at least 6 characters long",isLoading:!1},!0));if(this.data.password!==this.data.confirmPassword)return void(await this.updateData({error:"Passwords do not match",isLoading:!1},!0));const s=this.getApp().auth;if(!s)return void(await this.updateData({error:"Authentication system not available",isLoading:!1},!0));const t={name:this.data.name.trim(),email:this.data.email.toLowerCase().trim(),password:this.data.password,acceptedTerms:this.data.acceptTerms},a=await s.register(t);a.success||await this.updateData({error:a.message||"Registration failed. Please try again.",isLoading:!1},!0)}async onActionLogin(e){e.preventDefault(),this.getApp().navigate("/login")}async onActionHandleKeyPress(e,s){if("Enter"===e.key){e.preventDefault();const t=["registerName","registerEmail","registerPassword","registerConfirmPassword"],a=t.indexOf(s.id);if(a>=0&&a<t.length-1){const e=this.element.querySelector(`#${t[a+1]}`);e&&e.focus()}else a===t.length-1&&await this.onActionRegister(e)}}async getViewData(){return{...this.data}}}class ForgotPasswordPage extends t.Page{static pageName="auth-forgot-password";static title="Forgot Password";static icon="bi-key";static route="forgot-password";constructor(e={}){super({...e,template:e.template}),this.authConfig=e.authConfig||{passwordResetMethod:"code",ui:{title:"My App"},features:{}}}async onInit(){this.data={...this.authConfig.ui,...this.authConfig.features,passwordResetMethod:this.authConfig.passwordResetMethod,step:"email",isLoading:!1,error:null,email:""}}async onEnter(){document.title=`${ForgotPasswordPage.title} - ${this.authConfig.ui.title}`,this.updateData({step:"email",isLoading:!1,error:null,email:""})}getFormData(e){const s=this.element.querySelector(e);if(!s)return{};const t=new FormData(s);return Object.fromEntries(t.entries())}async onActionRequestReset(){const{email:e}=this.getFormData("#form-request-reset");if(await this.updateData({isLoading:!0,error:null,email:e},!0),!e)return this.updateData({error:"Please enter your email address",isLoading:!1},!0);const s=this.getApp().auth,t=this.authConfig.passwordResetMethod||"code",a=await s.forgotPassword(e,t);"link"===t?(await this.updateData({step:"link_sent",isLoading:!1},!0),a.success||console.error("Forgot password (link) error:",a.message)):a.success?await this.updateData({step:"code",isLoading:!1},!0):await this.updateData({error:a.message,isLoading:!1},!0)}async onActionResetWithCode(){const{code:e,new_password:s,confirm_password:t}=this.getFormData("#form-reset-with-code");if(await this.updateData({isLoading:!0,error:null},!0),!e||!s)return this.updateData({error:"Please enter the code and your new password",isLoading:!1},!0);if(s!==t)return this.updateData({error:"Passwords do not match",isLoading:!1},!0);const a=this.getApp().auth,n=await a.resetPasswordWithCode(this.data.email,e,s);n.success?(await this.updateData({step:"success",isLoading:!1},!0),setTimeout(()=>this.getApp().navigate("/login"),3e3)):await this.updateData({error:n.message,isLoading:!1},!0)}async onActionBackToLogin(){this.getApp().navigate("/login")}get isStepEmail(){return"email"===this.data.step}get isStepCode(){return"code"===this.data.step}get isStepLinkSent(){return"link_sent"===this.data.step}get isStepSuccess(){return"success"===this.data.step}}class ResetPasswordPage extends t.Page{static pageName="auth-reset-password";static title="Reset Password";static icon="bi-key-fill";static route="reset-password";constructor(e={}){super({...e,pageName:ResetPasswordPage.pageName,route:e.route||ResetPasswordPage.route,pageIcon:ResetPasswordPage.icon,template:"auth/pages/ResetPasswordPage.mst"}),this.authConfig=e.authConfig||{ui:{title:"My App",logoUrl:"/assets/logo.png",messages:{resetTitle:"Set New Password",resetSubtitle:"Choose a strong password"}},features:{registration:!0}},this.resetToken=null}async onInit(){await super.onInit(),this.data={...this.authConfig.ui,...this.authConfig.features,password:"",confirmPassword:"",resetToken:"",isLoading:!1,error:null,success:!1,successMessage:null,showPassword:!1,showConfirmPassword:!1,passwordStrength:null,passwordMatch:!0,tokenValid:!1}}async onEnter(){await super.onEnter(),document.title=`${ResetPasswordPage.title} - ${this.authConfig.ui.title}`;const e=new URLSearchParams(window.location.search);this.resetToken=e.get("token")||"",this.resetToken?this.updateData({resetToken:this.resetToken,tokenValid:!0,error:null,success:!1}):this.updateData({error:"Invalid or missing reset token. Please request a new password reset.",tokenValid:!1})}async onAfterRender(){if(await super.onAfterRender(),this.data.tokenValid){const e=this.element.querySelector("#resetPassword");e&&e.focus()}}async onActionUpdateField(e,s){const t=s.dataset.field,a=s.value;this.updateData({[t]:a,error:null}),"password"===t&&this.checkPasswordStrength(a),"password"!==t&&"confirmPassword"!==t||this.checkPasswordMatch()}checkPasswordStrength(e){let s=null;s=0===e.length?null:e.length<6?"weak":e.length<8?"fair":/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/.test(e)?"strong":/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(e)?"good":"fair",this.updateData({passwordStrength:s},!0)}checkPasswordMatch(){const e=!this.data.confirmPassword||this.data.password===this.data.confirmPassword;this.updateData({passwordMatch:e},!0)}async onActionTogglePassword(e,s){e.preventDefault();const t=s.dataset.passwordField;if("password"===t){this.updateData({showPassword:!this.data.showPassword});const e=this.element.querySelector("#resetPassword");e&&(e.type=this.data.showPassword?"text":"password")}else if("confirmPassword"===t){this.updateData({showConfirmPassword:!this.data.showConfirmPassword});const e=this.element.querySelector("#resetConfirmPassword");e&&(e.type=this.data.showConfirmPassword?"text":"password")}}async onActionResetPassword(e){if(e.preventDefault(),await this.updateData({error:null,isLoading:!0},!0),!this.data.password||!this.data.confirmPassword)return void(await this.updateData({error:"Please enter and confirm your new password",isLoading:!1},!0));if(this.data.password.length<6)return void(await this.updateData({error:"Password must be at least 6 characters long",isLoading:!1},!0));if(this.data.password!==this.data.confirmPassword)return void(await this.updateData({error:"Passwords do not match",isLoading:!1},!0));const s=this.getApp().auth;if(!s)return void(await this.updateData({error:"Authentication system not available",isLoading:!1},!0));const t=await s.resetPasswordWithToken(this.resetToken,this.data.password);t.success?(await this.updateData({success:!0,successMessage:t.message||"Password reset successful! You can now log in.",isLoading:!1},!0),setTimeout(()=>{this.getApp().showSuccess("Password reset complete. Please log in."),this.getApp().navigate("/login")},3e3)):await this.updateData({error:t.message||"Password reset failed. Please try again.",isLoading:!1},!0)}async onActionBackToLogin(e){e.preventDefault(),this.getApp().navigate("/login")}async onActionRegister(e){e.preventDefault(),this.getApp().navigate("/register")}async onActionRequestNew(e){e.preventDefault(),this.getApp().navigate("/forgot-password")}async onActionHandleKeyPress(e,s){if("Enter"===e.key){e.preventDefault();const t=["resetPassword","resetConfirmPassword"],a=t.indexOf(s.id);if(a>=0&&a<t.length-1){const e=this.element.querySelector(`#${t[a+1]}`);e&&e.focus()}else a===t.length-1&&await this.onActionResetPassword(e)}}async getViewData(){return{...this.data}}}const a={};function n(e){const s=e.replace(/^\//,"").replace(/^src\//,"").replace(/\\/g,"/");return a[s]||a[e]}a["extensions/auth/pages/ForgotPasswordPage.mst"]='<div class="auth-page forgot-password-page min-vh-100 d-flex align-items-center py-4">\n <div class="container">\n <div class="row justify-content-center">\n <div class="col-sm-8 col-md-8 col-lg-6 col-xl-5">\n <div class="card shadow-lg border-0">\n <div class="card-body p-4 p-md-5">\n \x3c!-- Header --\x3e\n <div class="text-center mb-4">\n {{#logoUrl}}<img src="{{logoUrl}}" alt="{{title}}" class="mb-3" style="max-height: 60px;">{{/logoUrl}}\n <h2 class="h3 mb-2">{{messages.forgotTitle}}</h2>\n <p class="text-muted">{{messages.forgotSubtitle}}</p>\n </div>\n\n \x3c!-- Error Alert --\x3e\n {{#error}}\n <div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">\n <i class="bi bi-exclamation-triangle-fill me-2"></i>\n <div>{{error}}</div>\n <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>\n </div>\n {{/error}}\n\n \x3c!-- Step 1: Email Form --\x3e\n {{#isStepEmail}}\n <form id="form-request-reset" novalidate>\n <div class="mb-3">\n <label for="forgotEmail" class="form-label"><i class="bi bi-envelope me-1"></i>Email Address</label>\n <input type="email" class="form-control form-control-lg" id="forgotEmail" name="email" placeholder="Enter your registered email" required autofocus>\n <div class="form-text">We\'ll send you instructions to reset your password.</div>\n </div>\n <button type="button" class="btn btn-primary btn-lg w-100 mb-3" data-action="requestReset" {{#isLoading}}disabled{{/isLoading}}>\n {{#isLoading}}<span class="spinner-border spinner-border-sm me-2"></span>Sending...{{/isLoading}}\n {{^isLoading}}<i class="bi bi-send me-2"></i>Send Instructions{{/isLoading}}\n </button>\n <button type="button" class="btn btn-outline-secondary btn-lg w-100" data-action="backToLogin" {{#isLoading}}disabled{{/isLoading}}>\n <i class="bi bi-arrow-left me-2"></i>Back to Login\n </button>\n </form>\n <div class="text-center mt-4">\n <p class="text-muted">Remember your password? <a href="#" class="text-decoration-none fw-semibold" data-action="backToLogin">Sign in</a></p>\n </div>\n {{/isStepEmail}}\n\n \x3c!-- Step 2 (Link Method): Confirmation --\x3e\n {{#isStepLinkSent}}\n <div class="text-center">\n <div class="mb-4"><i class="bi bi-envelope-check text-success" style="font-size: 4rem;"></i></div>\n <h3 class="h4 mb-3">Check your email</h3>\n <p class="text-muted">If an account exists for <strong>{{data.email}}</strong>, we have sent instructions for resetting your password.</p>\n <div class="d-grid gap-2 mt-4">\n <button type="button" class="btn btn-primary btn-lg" data-action="backToLogin"><i class="bi bi-arrow-left me-2"></i>Back to Login</button>\n </div>\n </div>\n {{/isStepLinkSent}}\n\n \x3c!-- Step 2 (Code Method): Code Entry Form --\x3e\n {{#isStepCode}}\n <p class="text-muted text-center mb-3">A verification code has been sent to <strong>{{data.email}}</strong>. Please enter it below.</p>\n <form id="form-reset-with-code" novalidate>\n <div class="mb-3">\n <label for="resetCode" class="form-label"><i class="bi bi-shield-lock me-1"></i>Verification Code</label>\n <input type="text" class="form-control form-control-lg" id="resetCode" name="code" placeholder="Enter code" required>\n </div>\n <div class="mb-3">\n <label for="resetPassword" class="form-label"><i class="bi bi-lock me-1"></i>New Password</label>\n <input type="password" class="form-control form-control-lg" id="resetPassword" name="new_password" placeholder="Enter new password" required autocomplete="new-password">\n </div>\n <div class="mb-3">\n <label for="confirmPassword" class="form-label"><i class="bi bi-lock-fill me-1"></i>Confirm New Password</label>\n <input type="password" class="form-control form-control-lg" id="confirmPassword" name="confirm_password" placeholder="Confirm new password" required autocomplete="new-password">\n </div>\n <button type="button" class="btn btn-primary btn-lg w-100" data-action="resetWithCode" {{#isLoading}}disabled{{/isLoading}}>\n {{#isLoading}}<span class="spinner-border spinner-border-sm me-2"></span>Resetting...{{/isLoading}}\n {{^isLoading}}<i class="bi bi-key me-2"></i>Reset Password{{/isLoading}}\n </button>\n </form>\n {{/isStepCode}}\n\n \x3c!-- Step 3 (Code Method): Success --\x3e\n {{#isStepSuccess}}\n <div class="text-center">\n <div class="mb-4"><i class="bi bi-check-circle text-success" style="font-size: 4rem;"></i></div>\n <h3 class="h4 mb-3">Password Reset!</h3>\n <p class="text-muted">Your password has been changed successfully. You will be redirected to the login page shortly.</p>\n </div>\n {{/isStepSuccess}}\n\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n',a["extensions/auth/pages/LoginPage.mst"]='<div class="auth-page min-vh-100 d-flex align-items-center py-4">\n <div class="container">\n <div class="row justify-content-center">\n <div class="col-sm-10 col-md-8 col-lg-6 col-xl-5">\n <div class="card shadow-lg border-0">\n <div class="card-body p-4 p-md-5">\n \x3c!-- Logo and Header --\x3e\n <div class="text-center mb-4">\n {{#data.logoUrl}}\n <img src="{{data.logoUrl}}" alt="{{data.title}}" class="mb-3" style="max-height: 60px;">\n {{/data.logoUrl}}\n {{#data.messages.loginTitle}}\n <h2 class="h3 mb-2">{{#data.loginIcon}}<i class="{{data.loginIcon}}"></i> {{/data.loginIcon}}{{data.messages.loginTitle}}</h2>\n {{/data.messages.loginTitle}}\n {{/data.messages.loginSubtitle}}\n <p class="text-muted">{{data.messages.loginSubtitle}}</p>\n {{/data.messages.loginSubtitle}}\n </div>\n\n \x3c!-- Error Alert --\x3e\n {{#data.error}}\n <div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">\n <i class="bi bi-exclamation-triangle-fill me-2"></i>\n <div>{{data.error}}</div>\n <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>\n </div>\n {{/data.error}}\n\n \x3c!-- Login Form --\x3e\n <form novalidate>\n \x3c!-- Username/Email Field --\x3e\n <div class="mb-3">\n <label for="loginUsername" class="form-label">\n <i class="bi bi-person me-1"></i>Username or Email\n </label>\n <input\n type="text"\n class="form-control form-control-lg"\n id="loginUsername"\n placeholder="Enter your username or email"\n value="{{username}}"\n data-field="username"\n data-action-keydown="handleKeyPress"\n autocomplete="username"\n required\n autofocus\n {{#isLoading}}disabled{{/isLoading}}>\n </div>\n\n \x3c!-- Password Field --\x3e\n <div class="mb-3">\n <label for="loginPassword" class="form-label">\n <i class="bi bi-lock me-1"></i>Password\n </label>\n <div class="input-group">\n <input\n type="{{#showPassword}}text{{/showPassword}}{{^showPassword}}password{{/showPassword}}"\n class="form-control form-control-lg"\n id="loginPassword"\n placeholder="Enter your password"\n value="{{password}}"\n data-field="password"\n data-action-keydown="handleKeyPress"\n autocomplete="current-password"\n required\n {{#isLoading}}disabled{{/isLoading}}>\n <button\n class="btn btn-outline-secondary"\n type="button"\n data-action="togglePassword"\n {{#isLoading}}disabled{{/isLoading}}>\n <i class="bi bi-eye{{#data.showPassword}}-slash{{/data.showPassword}}"></i>\n </button>\n </div>\n </div>\n\n \x3c!-- Remember Me & Forgot Password --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n {{#data.rememberMe}}\n <div class="form-check">\n <input\n class="form-check-input"\n type="checkbox"\n id="rememberMe"\n data-field="rememberMe"\n data-change-action="updateField"\n autocomplete="off"\n {{#rememberMe}}checked{{/rememberMe}}\n {{#isLoading}}disabled{{/isLoading}}>\n <label class="form-check-label" for="rememberMe">\n Remember me\n </label>\n </div>\n {{/data.rememberMe}}\n {{^data.rememberMe}}<div></div>{{/data.rememberMe}}\n\n {{#data.forgotPassword}}\n <a href="?page=forgot-password" class="text-decoration-none" data-action="forgotPassword">\n Forgot password?\n </a>\n {{/data.forgotPassword}}\n </div>\n\n \x3c!-- Login Button --\x3e\n <button\n type="button"\n class="btn btn-primary btn-lg w-100 mb-3"\n data-action="login"\n {{#data.isLoading}}disabled{{/data.isLoading}}>\n {{#data.isLoading}}\n <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>\n Signing in...\n {{/data.isLoading}}\n {{^data.isLoading}}\n <i class="bi bi-box-arrow-in-right me-2"></i>Sign In\n {{/data.isLoading}}\n </button>\n\n \x3c!-- Alternative Login Methods --\x3e\n {{#data.passkeySupported}}\n <div class="position-relative my-3">\n <hr class="text-muted">\n <span class="position-absolute top-50 start-50 translate-middle bg-white px-3 text-muted small">\n OR\n </span>\n </div>\n\n <button\n type="button"\n class="btn btn-outline-primary btn-lg w-100 mb-2"\n data-action="loginWithPasskey"\n {{#data.isLoading}}disabled{{/data.isLoading}}>\n <i class="bi bi-fingerprint me-2"></i>Sign in with Passkey\n </button>\n {{/data.passkeySupported}}\n </form>\n\n \x3c!-- Register Link --\x3e\n {{#data.registration}}\n <div class="text-center mt-4">\n <p class="mb-0">\n Don\'t have an account?\n <a href="#" class="text-decoration-none fw-semibold" data-action="register">\n Sign up\n </a>\n </p>\n </div>\n {{/data.registration}}\n </div>\n </div>\n\n \x3c!-- Security Notice --\x3e\n \x3c!-- TOS and Privacy Links --\x3e\n <div class="text-center mt-3 auth-footer-links">\n {{#data.termsUrl}}\n <small><a href="{{data.termsUrl}}" target="_blank" rel="noopener noreferrer">Terms of Service</a></small>\n {{/data.termsUrl}}\n {{#data.termsUrl}}{{#data.privacyUrl}}\n <small class="mx-1 text-muted">·</small>\n {{/data.privacyUrl}}{{/data.termsUrl}}\n {{#data.privacyUrl}}\n <small><a href="{{data.privacyUrl}}" target="_blank" rel="noopener noreferrer">Privacy Policy</a></small>\n {{/data.privacyUrl}}\n <div class="text-muted text-center mt-3">\n <small>version {{data.version}}</small>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n',a["extensions/auth/pages/RegisterPage.mst"]='<div class="auth-page register-page min-vh-100 d-flex align-items-center py-4">\n <div class="container">\n <div class="row justify-content-center">\n <div class="col-sm-10 col-md-8 col-lg-6 col-xl-5">\n <div class="card shadow-lg border-0">\n <div class="card-body p-4 p-md-5">\n \x3c!-- Logo and Header --\x3e\n <div class="text-center mb-4">\n {{#logoUrl}}\n <img src="{{logoUrl}}" alt="{{title}}" class="mb-3" style="max-height: 60px;">\n {{/logoUrl}}\n <h2 class="h3 mb-2">{{messages.registerTitle}}</h2>\n <p class="text-muted">{{messages.registerSubtitle}}</p>\n </div>\n\n \x3c!-- Error Alert --\x3e\n {{#error}}\n <div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">\n <i class="bi bi-exclamation-triangle-fill me-2"></i>\n <div>{{error}}</div>\n <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>\n </div>\n {{/error}}\n\n \x3c!-- Registration Form --\x3e\n <form data-action="register" novalidate>\n \x3c!-- Name Field --\x3e\n <div class="mb-3">\n <label for="registerName" class="form-label">\n <i class="bi bi-person me-1"></i>Full Name\n </label>\n <input\n type="text"\n class="form-control form-control-lg"\n id="registerName"\n placeholder="Enter your full name"\n value="{{name}}"\n data-field="name"\n data-change-action="updateField"\n data-filter="live-search"\n data-action-keydown="handleKeyPress"\n autocomplete="name"\n required\n autofocus\n {{#isLoading}}disabled{{/isLoading}}>\n </div>\n\n \x3c!-- Email Field --\x3e\n <div class="mb-3">\n <label for="registerEmail" class="form-label">\n <i class="bi bi-envelope me-1"></i>Email Address\n </label>\n <input\n type="email"\n class="form-control form-control-lg"\n id="registerEmail"\n placeholder="name@example.com"\n value="{{email}}"\n data-field="email"\n data-change-action="updateField"\n data-filter="live-search"\n data-action-keydown="handleKeyPress"\n autocomplete="email"\n required\n {{#isLoading}}disabled{{/isLoading}}>\n </div>\n\n \x3c!-- Password Field --\x3e\n <div class="mb-3">\n <label for="registerPassword" class="form-label">\n <i class="bi bi-lock me-1"></i>Password\n </label>\n <div class="input-group">\n <input\n type="{{#showPassword}}text{{/showPassword}}{{^showPassword}}password{{/showPassword}}"\n class="form-control form-control-lg"\n id="registerPassword"\n placeholder="Create a strong password"\n value="{{password}}"\n data-field="password"\n data-change-action="updateField"\n data-filter="live-search"\n data-action-keydown="handleKeyPress"\n autocomplete="new-password"\n required\n {{#isLoading}}disabled{{/isLoading}}>\n <button\n class="btn btn-outline-secondary"\n type="button"\n data-password-field="password"\n data-action="togglePassword"\n {{#isLoading}}disabled{{/isLoading}}>\n <i class="bi bi-eye{{#showPassword}}-slash{{/showPassword}}"></i>\n </button>\n </div>\n\n \x3c!-- Password Strength Indicator --\x3e\n {{#passwordStrength}}\n <div class="mt-2">\n <div class="progress" style="height: 4px;">\n <div class="progress-bar\n {{#passwordStrength.weak}}bg-danger{{/passwordStrength.weak}}\n {{#passwordStrength.fair}}bg-warning{{/passwordStrength.fair}}\n {{#passwordStrength.good}}bg-info{{/passwordStrength.good}}\n {{#passwordStrength.strong}}bg-success{{/passwordStrength.strong}}"\n role="progressbar"\n style="width:\n {{#passwordStrength.weak}}25%{{/passwordStrength.weak}}\n {{#passwordStrength.fair}}50%{{/passwordStrength.fair}}\n {{#passwordStrength.good}}75%{{/passwordStrength.good}}\n {{#passwordStrength.strong}}100%{{/passwordStrength.strong}}">\n </div>\n </div>\n <small class="text-muted mt-1">\n Password strength: {{passwordStrength}}\n </small>\n </div>\n {{/passwordStrength}}\n </div>\n\n \x3c!-- Confirm Password Field --\x3e\n <div class="mb-3">\n <label for="registerConfirmPassword" class="form-label">\n <i class="bi bi-lock-fill me-1"></i>Confirm Password\n </label>\n <div class="input-group">\n <input\n type="{{#showConfirmPassword}}text{{/showConfirmPassword}}{{^showConfirmPassword}}password{{/showConfirmPassword}}"\n class="form-control form-control-lg {{^passwordMatch}}is-invalid{{/passwordMatch}}"\n id="registerConfirmPassword"\n placeholder="Re-enter your password"\n value="{{confirmPassword}}"\n data-field="confirmPassword"\n data-change-action="updateField"\n data-filter="live-search"\n data-action-keydown="handleKeyPress"\n autocomplete="new-password"\n required\n {{#isLoading}}disabled{{/isLoading}}>\n <button\n class="btn btn-outline-secondary"\n type="button"\n data-password-field="confirmPassword"\n data-action="togglePassword"\n {{#isLoading}}disabled{{/isLoading}}>\n <i class="bi bi-eye{{#showConfirmPassword}}-slash{{/showConfirmPassword}}"></i>\n </button>\n </div>\n {{^passwordMatch}}\n <div class="invalid-feedback">\n Passwords do not match\n </div>\n {{/passwordMatch}}\n </div>\n\n \x3c!-- Register Button --\x3e\n <button\n type="submit"\n class="btn btn-primary btn-lg w-100 mb-3"\n {{#isLoading}}disabled{{/isLoading}}>\n {{#isLoading}}\n <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>\n Creating account...\n {{/isLoading}}\n {{^isLoading}}\n <i class="bi bi-person-plus me-2"></i>Create Account\n {{/isLoading}}\n </button>\n </form>\n\n \x3c!-- Login Link --\x3e\n <div class="text-center mt-4">\n <p class="mb-0">\n Already have an account?\n <a href="#" class="text-decoration-none fw-semibold" data-action="login">\n Sign in\n </a>\n </p>\n </div>\n </div>\n </div>\n\n \x3c!-- Security Notice --\x3e\n <div class="text-center mt-3">\n <small class="text-muted">\n <i class="bi bi-shield-check me-1"></i>Your information is secure and encrypted\n </small>\n </div>\n </div>\n </div>\n </div>\n</div>\n',a["extensions/auth/pages/ResetPasswordPage.mst"]='<div class="auth-page reset-password-page min-vh-100 d-flex align-items-center py-4">\n <div class="container">\n <div class="row justify-content-center">\n <div class="col-sm-10 col-md-8 col-lg-6 col-xl-5">\n <div class="card shadow-lg border-0">\n <div class="card-body p-4 p-md-5">\n \x3c!-- Logo and Header --\x3e\n <div class="text-center mb-4">\n {{#logoUrl}}\n <img src="{{logoUrl}}" alt="{{title}}" class="mb-3" style="max-height: 60px;">\n {{/logoUrl}}\n <h2 class="h3 mb-2">{{messages.resetTitle}}</h2>\n <p class="text-muted">{{messages.resetSubtitle}}</p>\n </div>\n\n \x3c!-- Error Alert --\x3e\n {{#error}}\n <div class="alert alert-danger d-flex align-items-center alert-dismissible fade show" role="alert">\n <i class="bi bi-exclamation-triangle-fill me-2"></i>\n <div>{{error}}</div>\n <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>\n </div>\n {{/error}}\n\n \x3c!-- Success State --\x3e\n {{#success}}\n <div class="alert alert-success d-flex align-items-center" role="alert">\n <i class="bi bi-check-circle-fill me-2"></i>\n <div>\n <strong>Password Reset Complete!</strong><br>\n {{#successMessage}}{{successMessage}}{{/successMessage}}\n {{^successMessage}}Your password has been reset successfully. You can now log in with your new password.{{/successMessage}}\n </div>\n </div>\n\n <div class="text-center">\n <p class="mb-3">\n <i class="bi bi-shield-check text-success" style="font-size: 3rem;"></i>\n </p>\n <p class="text-muted">\n Redirecting you to the login page...\n </p>\n <div class="d-grid gap-2 mt-4">\n <button\n class="btn btn-primary btn-lg"\n data-action="backToLogin">\n <i class="bi bi-box-arrow-in-right me-2"></i>Continue to Login\n </button>\n </div>\n </div>\n {{/success}}\n\n \x3c!-- Reset Form --\x3e\n {{^success}}\n {{#tokenValid}}\n <form data-action="resetPassword" novalidate>\n \x3c!-- New Password Field --\