diff --git a/app/services/credential_service.py b/app/services/credential_service.py
index 9b99333..06a2382 100644
--- a/app/services/credential_service.py
+++ b/app/services/credential_service.py
@@ -83,7 +83,7 @@ def list_credentials(
login_type: Optional[str] = None,
has_otp: Optional[bool] = None,
) -> List[CredentialRead]:
- """凭据列表:搜索 site/username/note,可按登录方式与 2FA 绑定过滤"""
+ """凭据列表:搜索 site/username/note/授权来源,可按登录方式与 2FA 绑定过滤"""
stmt = select(Credential)
if login_type:
stmt = stmt.where(Credential.login_type == login_type)
@@ -98,6 +98,7 @@ def list_credentials(
Credential.site.like(like), # type: ignore[union-attr]
Credential.username.like(like), # type: ignore[union-attr]
Credential.note.like(like), # type: ignore[union-attr]
+ Credential.oauth_provider.like(like), # type: ignore[union-attr]
)
)
creds = list(
diff --git a/static/js/api.js b/static/js/api.js
index dd253ad..2cd37a4 100644
--- a/static/js/api.js
+++ b/static/js/api.js
@@ -28,6 +28,49 @@ window.VpsApi = (function () {
};
})();
+/* 授权登录来源预设:key = 入库值(oauth_provider,统一英文 slug),label = 展示名,
+ * group = 表单分组顺序。国内三方登录排在最前(支付宝 / 淘宝 / 微博 等),
+ * 手填的自定义值由 store.oauthCustom 追加成候选,不需要动后端字典。 */
+const OAUTH_PRESETS = [
+ { key: 'alipay', label: '支付宝', group: '国内' },
+ { key: 'taobao', label: '淘宝', group: '国内' },
+ { key: 'wechat', label: '微信', group: '国内' },
+ { key: 'qq', label: 'QQ', group: '国内' },
+ { key: 'weibo', label: '微博', group: '国内' },
+ { key: 'douyin', label: '抖音', group: '国内' },
+ { key: 'baidu', label: '百度', group: '国内' },
+ { key: 'jd', label: '京东', group: '国内' },
+ { key: 'xiaohongshu', label: '小红书', group: '国内' },
+ { key: 'bilibili', label: 'B站', group: '国内' },
+ { key: 'meituan', label: '美团', group: '国内' },
+ { key: 'dingtalk', label: '钉钉', group: '国内' },
+ { key: 'feishu', label: '飞书', group: '国内' },
+ { key: 'huawei', label: '华为', group: '国内' },
+ { key: 'mi', label: '小米', group: '国内' },
+ { key: 'google', label: 'Google', group: '国际' },
+ { key: 'apple', label: 'Apple', group: '国际' },
+ { key: 'microsoft', label: 'Microsoft', group: '国际' },
+ { key: 'facebook', label: 'Facebook', group: '国际' },
+ { key: 'x', label: 'X (Twitter)', group: '国际' },
+ { key: 'yahoo', label: 'Yahoo', group: '国际' },
+ { key: 'line', label: 'LINE', group: '国际' },
+ { key: 'kakao', label: 'Kakao', group: '国际' },
+ { key: 'naver', label: 'NAVER', group: '国际' },
+ { key: 'amazon', label: 'Amazon', group: '国际' },
+ { key: 'paypal', label: 'PayPal', group: '国际' },
+ { key: 'discord', label: 'Discord', group: '国际' },
+ { key: 'telegram', label: 'Telegram', group: '国际' },
+ { key: 'reddit', label: 'Reddit', group: '国际' },
+ { key: 'linkedin', label: 'LinkedIn', group: '国际' },
+ { key: 'steam', label: 'Steam', group: '国际' },
+ { key: 'epic', label: 'Epic Games', group: '国际' },
+ { key: 'github', label: 'GitHub', group: '开发者' },
+ { key: 'gitlab', label: 'GitLab', group: '开发者' },
+ { key: 'bitbucket', label: 'Bitbucket', group: '开发者' },
+ { key: 'cloudflare', label: 'Cloudflare', group: '开发者' },
+ { key: 'huggingface', label: 'Hugging Face', group: '开发者' },
+];
+
window.VpsFmt = {
TYPE_LABELS: { vps: 'VPS', domain: '域名', ai_agent: 'AI账号', cloudflare: 'Cloudflare', other: '其他' },
STATUS_LABELS: { active: '使用中', expired: '已过期', stopped: '已停止', cancelled: '已注销', unknown: '未知' },
@@ -37,8 +80,29 @@ window.VpsFmt = {
CYCLE_LABELS: { monthly: '月付', quarterly: '季付', yearly: '年付' },
// 凭据库登录方式:字段对应 Credential.login_type
LOGIN_TYPE_LABELS: { password: '密码登录', oauth: '授权登录', other: '其他' },
- // 授权登录常见来源(表单 datalist 建议,可自由输入)
- OAUTH_PROVIDERS: ['google', 'apple', 'github', 'microsoft', 'wechat', 'qq'],
+ /* 授权登录来源候选(表单 chips + datalist 建议)
+ *
+ * 清单在上面 OAUTH_PRESETS;这里只暴露给视图使用。
+ */
+ OAUTH_PRESETS,
+ // 短展示名:命中预设给中文,否则原样返回(自定义值不加工)
+ oauthLabel(k) {
+ const p = OAUTH_PRESETS.find(x => x.key === (k || '').trim().toLowerCase());
+ return p ? p.label : (k || '');
+ },
+ // 详情展示:「支付宝(alipay)」,自定义值只显示原文
+ oauthDetail(k) {
+ const p = OAUTH_PRESETS.find(x => x.key === (k || '').trim().toLowerCase());
+ return p ? p.label + '(' + p.key + ')' : (k || '');
+ },
+ // 归一化:允许输入中文名或大小写不一致的 slug,落到规范 key 上,避免同来源存成多种写法
+ normalizeOauth(raw) {
+ const v = (raw || '').trim();
+ if (!v) return '';
+ const lower = v.toLowerCase();
+ const p = OAUTH_PRESETS.find(x => x.key === lower || x.label.toLowerCase() === lower);
+ return p ? p.key : v;
+ },
loginTypeBadge(t) {
return { password: 'bg-slate-500/10 text-slate-600 dark:text-slate-400', oauth: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', other: 'bg-amber-500/10 text-amber-600 dark:text-amber-400' }[t] || 'bg-slate-500/10 text-slate-500';
},
diff --git a/static/js/modals.js b/static/js/modals.js
index 3decd0d..79b24a3 100644
--- a/static/js/modals.js
+++ b/static/js/modals.js
@@ -487,12 +487,36 @@ const CredentialModal = {
-
-
+
+
+
+
+
+
+
{{ g.name }}
+
+
+
+
+
+ 「{{ f.oauth_provider }}」不在候选里,会按原样保存,并记入上面的自定义候选
+
+
+