fix: 修复登录重复创建会话 + 备注输入框不可交互
- login.js 增加防重复提交锁,请求期间禁用按钮 - Manager.Create 增加幂等检查,同用户同IP同UA 10秒内复用已有会话 - 移除当前设备备注输入框的 readonly 限制,改为 data-current 标记 - 修复 readonly 状态下输入框完全透明无法点击的问题
This commit is contained in:
@ -2,6 +2,7 @@ package session
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"metazone.cc/metalab/internal/config"
|
||||
@ -18,15 +19,43 @@ type Manager struct {
|
||||
store Store
|
||||
userRepo userStore
|
||||
cfg *config.Config
|
||||
mu sync.Mutex
|
||||
recentLogin map[uint]*loginEntry // uid → 最近一次登录记录(防重复创建)
|
||||
}
|
||||
|
||||
type loginEntry struct {
|
||||
sid string
|
||||
ip string
|
||||
userAgent string
|
||||
createdAt time.Time
|
||||
}
|
||||
|
||||
// NewManager 构造函数
|
||||
func NewManager(store Store, userRepo userStore, cfg *config.Config) *Manager {
|
||||
return &Manager{store: store, userRepo: userRepo, cfg: cfg}
|
||||
return &Manager{
|
||||
store: store,
|
||||
userRepo: userRepo,
|
||||
cfg: cfg,
|
||||
recentLogin: make(map[uint]*loginEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建会话并写入存储
|
||||
// Create 创建会话并写入存储(同用户+同 IP+同 UA 在 10 秒内重复调用时返回已有会话,防重复创建)
|
||||
func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string) (string, error) {
|
||||
m.mu.Lock()
|
||||
// 幂等检查:同用户+同 IP+同 UA,10 秒内不重复创建
|
||||
if entry, ok := m.recentLogin[user.ID]; ok {
|
||||
if entry.ip == ip && entry.userAgent == userAgent && time.Since(entry.createdAt) < 10*time.Second {
|
||||
// 验证旧会话仍然有效
|
||||
if s, _ := m.store.Get(entry.sid); s != nil {
|
||||
m.mu.Unlock()
|
||||
log.Printf("[SessionManager] Create dedup uid=%d reuse sid=%s", user.ID, entry.sid[:16]+"...")
|
||||
return entry.sid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
sid, err := NewID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -47,6 +76,11 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string
|
||||
if err := m.store.Set(s); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.recentLogin[user.ID] = &loginEntry{sid: sid, ip: ip, userAgent: userAgent, createdAt: now}
|
||||
m.mu.Unlock()
|
||||
|
||||
log.Printf("[SessionManager] Created sid=%s uid=%d rememberMe=%v ip=%s", sid[:16]+"...", user.ID, rememberMe, ip)
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@
|
||||
<span class="session-time">最后活动 {{.LastAccess.Format "2006-01-02 15:04"}}</span>
|
||||
</div>
|
||||
<div class="session-remark-row">
|
||||
<input type="text" class="session-remark-input" value="{{.Remark}}" placeholder="添加备注(如 我的笔记本)" maxlength="32" data-sid="{{.ID}}" {{if .IsCurrent}}readonly{{end}}>
|
||||
<input type="text" class="session-remark-input" value="{{.Remark}}" placeholder="添加备注(如 我的笔记本)" maxlength="32" data-sid="{{.ID}}" data-current="{{.IsCurrent}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-action">
|
||||
|
||||
@ -690,15 +690,15 @@ body {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.session-remark-input[readonly] {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
cursor: default;
|
||||
.session-remark-input[data-current="true"] {
|
||||
background: rgba(46, 204, 113, .03);
|
||||
border-color: rgba(46, 204, 113, .2);
|
||||
}
|
||||
|
||||
.session-remark-input[readonly]:focus {
|
||||
box-shadow: none;
|
||||
border-color: transparent;
|
||||
.session-remark-input[data-current="true"]:focus {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px rgba(52, 152, 219, .15);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.session-action {
|
||||
|
||||
@ -55,7 +55,14 @@
|
||||
doLogin(email, password);
|
||||
});
|
||||
|
||||
var loginInProgress = false;
|
||||
|
||||
function doLogin(email, password) {
|
||||
if (loginInProgress) return;
|
||||
loginInProgress = true;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '登录中...';
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/auth/login', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
@ -65,7 +72,9 @@
|
||||
try { resp = JSON.parse(xhr.responseText); } catch (err) { resp = null; }
|
||||
|
||||
if (xhr.status === 200 && resp && resp.success && resp.action === 'confirm_restore') {
|
||||
// 注销账号登录 → 二次确认恢复
|
||||
loginInProgress = false;
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '登录';
|
||||
if (confirm(resp.message + '\n\n确认继续登录将中止注销流程。')) {
|
||||
doConfirmRestore(email, password);
|
||||
}
|
||||
@ -75,6 +84,9 @@
|
||||
window.location.href = '/';
|
||||
}, 800);
|
||||
} else {
|
||||
loginInProgress = false;
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '登录';
|
||||
var msg = (resp && resp.message) ? resp.message : '登录失败,请稍后重试';
|
||||
window.MetaLab.toast(toast, msg, true);
|
||||
}
|
||||
@ -84,6 +96,11 @@
|
||||
}
|
||||
|
||||
function doConfirmRestore(email, password) {
|
||||
if (loginInProgress) return;
|
||||
loginInProgress = true;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '恢复中...';
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/auth/confirm-restore', true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
@ -98,6 +115,9 @@
|
||||
window.location.href = '/';
|
||||
}, 800);
|
||||
} else {
|
||||
loginInProgress = false;
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = '登录';
|
||||
var msg = (resp && resp.message) ? resp.message : '操作失败,请稍后重试';
|
||||
window.MetaLab.toast(toast, msg, true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user