feat(vault): 授权来源候选扩至 37 项并支持手填自动记入
问题:凭据表单的「授权来源」只有 6 项 datalist 建议,缺国内常见的支付宝/淘宝/ 微博等三方登录,且移动端 iOS Safari 对 datalist 支持不稳,导致没法填。 - api.js: OAUTH_PROVIDERS(6) → OAUTH_PRESETS(37,按国内/国际/开发者分组, key 为入库值、label 为展示名);新增 oauthLabel/oauthDetail 展示中文名, normalizeOauth 把中文输入归一化成英文 slug,避免同一来源存成多种写法 - modals.js: 授权来源改为「输入框 + 分组芯片」选择器,随输入过滤(中英文均可 搜)、可一键清空、误填项可划掉;无匹配时提示将按原样保存 - store.js: 手填的非预设值记入 localStorage 候选,并在拉取凭据列表后与服务端 值对账,换设备也能看到以前填过的来源;被划掉的记入 ignore 不再冒回 - credential_service.py: 凭据库搜索纳入 oauth_provider
This commit is contained in:
+62
-1
@@ -4,6 +4,25 @@ const Api = window.VpsApi;
|
||||
const Fmt = window.VpsFmt;
|
||||
|
||||
/* ---------------- 全局状态 ---------------- */
|
||||
|
||||
/* 手动填入的授权来源候选(本地留存)
|
||||
*
|
||||
* credentials 表只存条目本身,不维护来源字典;为了让「这次手填的值下次还能点」,
|
||||
* 把非预设值记在 localStorage:oauth_custom 是候选队列,oauth_ignored 是被用户
|
||||
* 划掉过的值(防止从凭据列表反推时又冒回来)。
|
||||
*/
|
||||
const OAUTH_CUSTOM_KEY = 'vps_oauth_custom';
|
||||
const OAUTH_IGNORED_KEY = 'vps_oauth_ignored';
|
||||
function _readOauthList(k) {
|
||||
try {
|
||||
const raw = JSON.parse(localStorage.getItem(k) || '[]');
|
||||
return Array.isArray(raw) ? raw.filter(x => typeof x === 'string' && x) : [];
|
||||
} catch (e) { return []; }
|
||||
}
|
||||
function _writeOauthList(k, list) {
|
||||
try { localStorage.setItem(k, JSON.stringify(list.slice(0, 24))); } catch (e) { /* 隐私模式写入失败只影响候选展示 */ }
|
||||
}
|
||||
|
||||
const store = reactive({
|
||||
view: 'dashboard',
|
||||
assets: [],
|
||||
@@ -34,6 +53,8 @@ const store = reactive({
|
||||
credentialSaving: false, // 保存锁,防双击重复提交
|
||||
vaultSearch: '',
|
||||
vaultOtpOnly: false, // 只看已绑定 2FA 的条目
|
||||
oauthCustom: _readOauthList(OAUTH_CUSTOM_KEY), // 手填来源候选(预设之外)
|
||||
oauthIgnored: _readOauthList(OAUTH_IGNORED_KEY), // 已划掉的手填来源
|
||||
});
|
||||
|
||||
function applyDark() {
|
||||
@@ -96,6 +117,7 @@ async function loadCredentials() {
|
||||
if (store.vaultSearch) params.set('q', store.vaultSearch);
|
||||
if (store.vaultOtpOnly) params.set('has_otp', 'true');
|
||||
store.credentials = await Api.get('/credentials?' + params.toString());
|
||||
syncOauthCustom();
|
||||
}
|
||||
async function loadSubdomains() { store.subdomains = await Api.get('/subdomains'); }
|
||||
async function loadSiteCerts() { store.siteCerts = await Api.get('/site-certs'); }
|
||||
@@ -224,6 +246,41 @@ async function deleteAccount(a) {
|
||||
}
|
||||
|
||||
/* ---------------- 凭据库(密码 + 2FA) ---------------- */
|
||||
// 手填来源记入候选:预设内的值不记(清单已在 api.js),并撤销先前的「划掉」
|
||||
function rememberOauthProvider(raw) {
|
||||
const key = Fmt.normalizeOauth(raw);
|
||||
if (!key || Fmt.OAUTH_PRESETS.some(p => p.key === key)) return;
|
||||
store.oauthIgnored = store.oauthIgnored.filter(x => x !== key);
|
||||
_writeOauthList(OAUTH_IGNORED_KEY, store.oauthIgnored);
|
||||
store.oauthCustom = [key, ...store.oauthCustom.filter(x => x !== key)];
|
||||
_writeOauthList(OAUTH_CUSTOM_KEY, store.oauthCustom);
|
||||
}
|
||||
// 划掉一个候选(错别字等):同时进 ignore 名单,避免从凭据列表反推时再冒回来
|
||||
function dropOauthProvider(raw) {
|
||||
const key = Fmt.normalizeOauth(raw);
|
||||
if (!key) return;
|
||||
store.oauthCustom = store.oauthCustom.filter(x => x !== key);
|
||||
_writeOauthList(OAUTH_CUSTOM_KEY, store.oauthCustom);
|
||||
if (!store.oauthIgnored.includes(key)) {
|
||||
store.oauthIgnored = [...store.oauthIgnored, key];
|
||||
_writeOauthList(OAUTH_IGNORED_KEY, store.oauthIgnored);
|
||||
}
|
||||
}
|
||||
// 候选跟服务端的值对账:换设备/别人录过的来源也能直接点(忽略名单优先)
|
||||
function syncOauthCustom() {
|
||||
const used = [];
|
||||
for (const c of store.credentials) {
|
||||
const key = Fmt.normalizeOauth(c.oauth_provider);
|
||||
if (!key || store.oauthIgnored.includes(key)) continue;
|
||||
if (Fmt.OAUTH_PRESETS.some(p => p.key === key)) continue;
|
||||
if (!used.includes(key)) used.push(key);
|
||||
}
|
||||
const merged = [...store.oauthCustom, ...used.filter(k => !store.oauthCustom.includes(k))];
|
||||
if (merged.length !== store.oauthCustom.length || merged.some((k, i) => k !== store.oauthCustom[i])) {
|
||||
store.oauthCustom = merged;
|
||||
_writeOauthList(OAUTH_CUSTOM_KEY, merged);
|
||||
}
|
||||
}
|
||||
function emptyCredentialForm() {
|
||||
return {
|
||||
site: '', username: '', login_type: 'password', oauth_provider: '',
|
||||
@@ -253,9 +310,12 @@ async function saveCredential() {
|
||||
store.credentialSaving = true;
|
||||
const f = store.credentialModal.form;
|
||||
const editing = store.credentialModal.editing;
|
||||
// 授权来源允许手填(中文/英文均可),归一化后回写表单,保证候选高亮与入库值一致
|
||||
const oauthKey = f.login_type === 'oauth' ? Fmt.normalizeOauth(f.oauth_provider) : '';
|
||||
f.oauth_provider = oauthKey;
|
||||
const payload = {
|
||||
site: f.site, username: f.username || null, login_type: f.login_type,
|
||||
oauth_provider: f.oauth_provider || null, url: f.url || null, note: f.note || null,
|
||||
oauth_provider: oauthKey || null, url: f.url || null, note: f.note || null,
|
||||
};
|
||||
try {
|
||||
if (editing) {
|
||||
@@ -272,6 +332,7 @@ async function saveCredential() {
|
||||
await Api.post('/credentials', payload);
|
||||
}
|
||||
store.credentialModal.show = false;
|
||||
if (oauthKey) rememberOauthProvider(oauthKey);
|
||||
// 账号侧 has_login_password / has_otp 来自关联凭据,需一并刷新
|
||||
await Promise.all([loadCredentials(), loadAccounts()]);
|
||||
} catch (e) { store.error = e.message; }
|
||||
|
||||
Reference in New Issue
Block a user