vdi-cloud/ui/terminal/login-interactions.js

188 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
'use strict';
const form = document.getElementById('auth-form');
const username = document.getElementById('username');
const password = document.getElementById('password');
const remember = document.getElementById('auth-remember');
const submit = document.getElementById('auth-submit');
const feedback = document.getElementById('auth-feedback');
const serviceButton = document.getElementById('auth-service');
const serviceName = document.getElementById('auth-service-name');
const subtitle = document.getElementById('auth-subtitle');
const passwordToggle = document.getElementById('auth-toggle-password');
const dialog = document.getElementById('auth-dialog');
const dialogContent = document.getElementById('auth-dialog-content');
const DEMO = Object.freeze({username:'demo', password:'Demo123!'});
const keys = {account:'yuexin-terminal-login-account',connection:'yuexin-terminal-login-connection'};
const escape = value => String(value).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const read = key => {try{return JSON.parse(localStorage.getItem(key)||'null');}catch{return null;}};
const write = (key,value) => {try{if(value===null)localStorage.removeItem(key);else localStorage.setItem(key,JSON.stringify(value));return true;}catch{return false;}};
function validEndpoint(value) {
try {
const u = new URL(value);
if (u.username || u.password || u.search || u.hash) return null;
if (u.protocol !== 'https:' && !(u.protocol === 'http:' && ['localhost','127.0.0.1','[::1]'].includes(u.hostname))) return null;
return u.href.replace(/\/$/,'');
} catch {return null;}
}
const savedConnection = read(keys.connection);
let connection = savedConnection?.mode === 'enterprise' && validEndpoint(savedConnection.url)
? {mode:'enterprise',url:validEndpoint(savedConnection.url),organization:String(savedConnection.organization||'').slice(0,80)}
: {mode:'cloud',url:'',organization:''};
let mode = connection.mode, serviceIntent = '', busy = false, loginTimer, modalTimer, modalBusy = false;
let credentials = null;
let serviceDraft = {url:connection.url,organization:connection.organization};
function restoreAccount() {
const saved = read(keys.account);
username.value = typeof saved?.username === 'string' ? saved.username.slice(0,100) : '';
remember.checked = saved?.remember !== false;
}
function updateService() {
serviceName.textContent = connection.mode === 'enterprise' ? '企业部署' : '云端服务';
serviceButton.setAttribute('aria-label',serviceName.textContent+',打开连接设置');
serviceButton.title = connection.mode === 'enterprise' ? connection.url : '云端服务';
subtitle.textContent = connection.mode === 'enterprise' ? '使用企业工作账号继续' : '使用云端工作账号继续';
}
function message(text,error = false) {
feedback.textContent = text;
feedback.classList.toggle('error',error);
}
function clearInvalid() {
username.removeAttribute('aria-invalid');password.removeAttribute('aria-invalid');
}
function setBusy(value) {
busy = value;
form.setAttribute('aria-busy',String(value));
form.querySelectorAll('input,button').forEach(el=>{el.disabled=value;});
serviceButton.disabled=value;
submit.innerHTML=value?'<span class="auth-spinner" aria-hidden="true"></span>登录中…':'登 录';
}
function concealPassword() {
password.type='password';passwordToggle.setAttribute('aria-pressed','false');passwordToggle.setAttribute('aria-label','显示密码');
}
function enterWorkspace(identity) {
clearTimeout(loginTimer);credentials=null;password.value='';concealPassword();setBusy(false);message('');
document.dispatchEvent(new CustomEvent('terminal:login',{detail:identity}));
}
function submitLogin(event) {
event.preventDefault();
if(busy)return;
clearInvalid();
const account=username.value.trim();
if(!account){username.setAttribute('aria-invalid','true');message('请输入工作账号',true);username.focus();return;}
if(!password.value){password.setAttribute('aria-invalid','true');message('请输入密码',true);password.focus();return;}
credentials={username:account,password:password.value};
setBusy(true);message('正在验证工作账号…');
loginTimer=setTimeout(()=>{
const accepted=credentials?.username===DEMO.username&&credentials?.password===DEMO.password;
credentials=null;setBusy(false);
if(!accepted){password.value='';concealPassword();password.setAttribute('aria-invalid','true');message('账号或密码不正确,请重试或查看体验账号',true);password.focus();return;}
write(keys.account,{username:remember.checked?account:'',remember:remember.checked});
enterWorkspace({name:account,mode:'account',service:connection.mode,organization:connection.organization});
},900);
}
const heading = (title,sub) => `<div class="auth-dialog-header"><div><h2 id="auth-dialog-title">${title}</h2>${sub?`<p class="auth-dialog-sub">${sub}</p>`:''}</div><button type="button" class="auth-dialog-close" data-auth-action="close" aria-label="关闭对话框">×</button></div>`;
const cancel = (label='取消') => `<button type="button" class="auth-dialog-button" data-auth-action="close">${label}</button>`;
function showDialog(html) {
clearTimeout(modalTimer);modalBusy=false;
dialogContent.innerHTML=html;
if(!dialog.open)dialog.showModal();
}
function closeDialog() {clearTimeout(modalTimer);modalBusy=false;dialog.close();}
function rememberDraft() {
const url=dialogContent.querySelector('#auth-enterprise-url'),org=dialogContent.querySelector('#auth-enterprise-org');
if(url){serviceDraft.url=url.value;serviceDraft.organization=org.value;}
}
function serviceDialog(intent='') {
serviceIntent=intent;mode=intent==='sso'?'enterprise':connection.mode;
serviceDraft={url:connection.url,organization:connection.organization};
renderService();
}
function renderService() {
showDialog(heading('连接设置','选择云端服务或企业部署入口。')+
`<div class="auth-dialog-tabs" role="tablist" aria-label="服务模式"><button type="button" role="tab" id="auth-tab-cloud" aria-controls="auth-service-form" tabindex="${mode==='cloud'?0:-1}" aria-selected="${mode==='cloud'}" data-auth-action="service-mode" data-mode="cloud">云端服务</button><button type="button" role="tab" id="auth-tab-enterprise" aria-controls="auth-service-form" tabindex="${mode==='enterprise'?0:-1}" aria-selected="${mode==='enterprise'}" data-auth-action="service-mode" data-mode="enterprise">企业部署</button></div>`+
`<form id="auth-service-form" role="tabpanel" aria-labelledby="auth-tab-${mode}" novalidate>${mode==='enterprise'?`<label class="auth-field" for="auth-enterprise-url">企业服务地址<input class="auth-input" id="auth-enterprise-url" name="url" type="url" placeholder="https://vdi.company.com" autocomplete="off" value="${escape(serviceDraft.url)}" aria-describedby="auth-service-error" required><small>使用 HTTPS 地址;HTTP 仅支持本机预览。</small></label><label class="auth-field" for="auth-enterprise-org">租户或组织标识 <small>选填</small><input class="auth-input" id="auth-enterprise-org" name="organization" maxlength="80" placeholder="例如:越欣科技" value="${escape(serviceDraft.organization)}" autocomplete="off"></label>`:`<div class="auth-note"><strong>云端服务</strong><p>使用工作账号登录云桌面,无需填写企业服务地址。</p></div>`}<p class="auth-dialog-error" id="auth-service-error" role="status" aria-live="polite"></p><div class="auth-dialog-actions">${cancel()}<button class="auth-dialog-button primary" type="submit">${serviceIntent==='sso'&&mode==='enterprise'?'保存并继续':'保存设置'}</button></div></form>`);
}
function saveService(event) {
event.preventDefault();
rememberDraft();
let url='';
if(mode==='enterprise'){
const input=dialogContent.querySelector('#auth-enterprise-url');url=validEndpoint(input.value.trim());
if(!url){input.setAttribute('aria-invalid','true');dialogContent.querySelector('#auth-service-error').textContent='请输入有效的 HTTPS 企业服务地址';input.focus();return;}
}
connection=mode==='enterprise'?{mode,url,organization:serviceDraft.organization.trim()}:{mode:'cloud',url:'',organization:''};
const stored=write(keys.connection,connection);updateService();
if(serviceIntent==='sso'&&connection.mode==='enterprise'){ssoDialog();return;}
closeDialog();message(stored?'连接设置已保存':'连接设置已应用,本次预览内有效');username.focus();
}
function demoDialog() {
showDialog(heading('体验账号','用于预览登录、桌面选择和连接设置流程。')+
`<dl class="auth-demo-credentials"><div><dt>工作账号</dt><dd>${DEMO.username}</dd></div><div><dt>密码</dt><dd>${DEMO.password}</dd></div></dl><div class="auth-note">这是交互原型的公开体验账号,未连接真实认证服务。请使用体验信息。</div><div class="auth-dialog-actions">${cancel('返回')}<button type="button" class="auth-dialog-button primary" data-auth-action="fill-demo">填入体验账号</button></div>`);
}
function forgotDialog() {
showDialog(heading('忘记密码','企业工作账号由管理员统一管理。')+
`<div class="auth-note"><strong>请联系越欣科技管理员重置密码</strong><p>可提供工作账号与设备序列号 VDI-8F42A1,协助管理员定位。</p></div><p class="auth-dialog-sub">原型体验账号无法修改密码,可重新查看体验信息。</p><div class="auth-dialog-actions">${cancel('返回登录')}<button class="auth-dialog-button primary" type="button" data-auth-action="demo">查看体验账号</button></div>`);
}
function ssoDialog() {
if(connection.mode!=='enterprise'){serviceDialog('sso');return;}
showDialog(heading('企业账号登录','通过企业统一身份进入云桌面。')+
`<dl class="auth-note"><div><dt>企业服务</dt><dd>${escape(connection.url)}</dd></div>${connection.organization?`<div><dt>组织</dt><dd>${escape(connection.organization)}</dd></div>`:''}<div><dt>体验身份</dt><dd>越欣科技 · 体验用户</dd></div></dl><p class="auth-dialog-sub">当前为统一身份登录预览,不会跳转或访问真实企业身份服务。</p><p class="auth-dialog-status" id="auth-sso-status" role="status" aria-live="polite"></p><div class="auth-dialog-actions">${cancel('取消登录')}<button class="auth-dialog-button primary" type="button" data-auth-action="sso-continue">使用体验身份继续</button></div>`);
}
function ssoContinue(button) {
if(modalBusy)return;
modalBusy=true;button.disabled=true;button.innerHTML='<span class="auth-spinner" aria-hidden="true"></span>登录中…';
dialogContent.querySelector('#auth-sso-status').textContent='正在验证企业身份…';
modalTimer=setTimeout(()=>{closeDialog();enterWorkspace({name:'企业体验用户',mode:'sso',service:'enterprise',organization:connection.organization});},900);
}
function offlineDialog() {
showDialog(heading('离线体验','先了解桌面列表和终端设置。')+
`<div class="auth-note"><strong>无需工作账号</strong><p>可预览桌面、收藏和连接设置,设备与桌面信息为示例。配置仅保存在当前浏览器,不连接真实远程桌面。</p></div><div class="auth-dialog-actions">${cancel()}<button class="auth-dialog-button primary" type="button" data-auth-action="offline-continue">进入离线体验</button></div>`);
}
form.addEventListener('submit',submitLogin);
form.addEventListener('input',event=>{
if(event.target===username||event.target===password){event.target.removeAttribute('aria-invalid');if(!busy)message('');}
});
remember.addEventListener('change',()=>{
if(!remember.checked)write(keys.account,{username:'',remember:false});
});
passwordToggle.addEventListener('click',()=>{
const reveal=password.type==='password';password.type=reveal?'text':'password';passwordToggle.setAttribute('aria-pressed',String(reveal));passwordToggle.setAttribute('aria-label',reveal?'隐藏密码':'显示密码');
});
serviceButton.addEventListener('click',()=>serviceDialog());
document.getElementById('auth-demo').addEventListener('click',demoDialog);
document.getElementById('auth-forgot').addEventListener('click',forgotDialog);
document.getElementById('auth-sso').addEventListener('click',ssoDialog);
document.getElementById('auth-offline').addEventListener('click',offlineDialog);
dialog.addEventListener('submit',event=>{if(event.target.id==='auth-service-form')saveService(event);});
dialog.addEventListener('input',event=>{event.target.removeAttribute('aria-invalid');const error=dialogContent.querySelector('#auth-service-error');if(error)error.textContent='';});
dialog.addEventListener('click',event=>{
const button=event.target.closest('[data-auth-action]');
if(!button||button.disabled)return;
switch(button.dataset.authAction){
case 'close':closeDialog();break;
case 'service-mode':rememberDraft();mode=button.dataset.mode;renderService();dialogContent.querySelector(`[data-mode="${mode}"]`).focus();break;
case 'fill-demo':username.value=DEMO.username;password.value=DEMO.password;concealPassword();clearInvalid();message('已填入体验账号,点击登录继续');closeDialog();submit.focus();break;
case 'demo':demoDialog();break;
case 'sso-continue':ssoContinue(button);break;
case 'offline-continue':closeDialog();enterWorkspace({name:'离线体验',mode:'offline',service:'offline',organization:'越欣科技'});break;
}
});
dialog.addEventListener('keydown',event=>{
const tab=event.target.closest('[role=tab]');
if(!tab||!['ArrowLeft','ArrowRight','Home','End'].includes(event.key))return;
event.preventDefault();rememberDraft();
mode=event.key==='Home'?'cloud':event.key==='End'?'enterprise':mode==='cloud'?'enterprise':'cloud';
renderService();dialogContent.querySelector(`[data-mode="${mode}"]`).focus();
});
dialog.addEventListener('cancel',()=>{clearTimeout(modalTimer);modalBusy=false;});
dialog.addEventListener('close',()=>{clearTimeout(modalTimer);modalBusy=false;});
document.addEventListener('terminal:logout',()=>{
clearTimeout(loginTimer);clearTimeout(modalTimer);credentials=null;setBusy(false);password.value='';concealPassword();clearInvalid();restoreAccount();message('已退出登录');username.focus();
});
document.addEventListener('terminal:open-workspace',()=>{
clearTimeout(loginTimer);credentials=null;setBusy(false);password.value='';concealPassword();if(dialog.open)closeDialog();
});
restoreAccount();updateService();
})();