fix: 修改邮箱同步到 assets/issue + 自适应布局 + 部署后自动批量同步
- api/auth/me PUT + api/admin/users PATCH:写 LLDAP 后同步更新 assets/issue SQLite - deploy-ai.sh:OA 部署后自动执行批量 email 同步(新服务器首次部署填充历史数据) - scripts/sync-emails-to-sites.js:批量同步工具脚本 - 全站自适应:移除 maxWidth 硬限制,Form 用 auto-fit grid,表格 overflow-x auto
This commit is contained in:
parent
ab25541200
commit
14abdff875
|
|
@ -0,0 +1,37 @@
|
|||
// 批量同步 LLDAP email → assets / issue 本地用户表
|
||||
// 每次 OA 部署后自动执行,确保新服务器历史数据也能填充
|
||||
const { exec } = require('child_process')
|
||||
const { promisify } = require('util')
|
||||
const e = promisify(exec)
|
||||
|
||||
const SITES = [
|
||||
{ name: 'assets', db: process.env.ASSETS_DB_PATH || '/data/other-sites/assets/assets.db' },
|
||||
{ name: 'issue', db: process.env.ISSUE_DB_PATH || '/data/other-sites/issue/issue.db' },
|
||||
]
|
||||
|
||||
async function main() {
|
||||
const r = await e(
|
||||
`docker exec lldap sqlite3 /data/users.db "SELECT user_id, email FROM users WHERE email != '';"`
|
||||
)
|
||||
const lines = r.stdout.trim().split('\n').filter(Boolean)
|
||||
let synced = 0
|
||||
|
||||
for (const line of lines) {
|
||||
const [user, mail] = line.split('|')
|
||||
const su = user.replace(/'/g, "''")
|
||||
const sm = (mail || '').replace(/'/g, "''")
|
||||
|
||||
for (const site of SITES) {
|
||||
try {
|
||||
await e(
|
||||
`sqlite3 "${site.db}" "UPDATE users SET email = '${sm}', updated_at = datetime('now', '+8 hours') WHERE username = '${su}';"`
|
||||
)
|
||||
} catch {}
|
||||
}
|
||||
synced++
|
||||
console.log(` ${user} → ${mail}`)
|
||||
}
|
||||
console.log(`已同步 ${synced} 个用户邮箱`)
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err.message); process.exit(1) })
|
||||
|
|
@ -9,10 +9,11 @@ interface LdapUser { username: string; email: string; displayName: string; creat
|
|||
|
||||
const s = {
|
||||
wrap: { minHeight: '100vh', background: 'var(--bg)' } as React.CSSProperties,
|
||||
main: { padding: '32px 40px', maxWidth: 1440 } as React.CSSProperties,
|
||||
tabBar: { display: 'flex', gap: 0, borderBottom: '1px solid var(--border)', marginBottom: 32 } as React.CSSProperties,
|
||||
main: { padding: 'clamp(16px, 3vw, 48px)' } as React.CSSProperties,
|
||||
tabBar: { display: 'flex', gap: 0, borderBottom: '1px solid var(--border)', marginBottom: 32, overflowX: 'auto' as any, whiteSpace: 'nowrap' as any } as React.CSSProperties,
|
||||
content: {} as React.CSSProperties,
|
||||
grid2: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 } as React.CSSProperties,
|
||||
grid2: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 20 } as React.CSSProperties,
|
||||
tableWrap: { overflowX: 'auto' as any } as React.CSSProperties,
|
||||
field: { marginBottom: 20 } as React.CSSProperties,
|
||||
label: { display: 'block', fontSize: 13, fontWeight: 500, color: 'var(--text-secondary)', marginBottom: 6 } as React.CSSProperties,
|
||||
input: { width: '100%', height: 44, padding: '0 14px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--bg-card)', color: 'var(--text)', fontSize: 14, outline: 'none', boxSizing: 'border-box' as any },
|
||||
|
|
@ -200,7 +201,7 @@ export default function AdminUsersPage() {
|
|||
{users.length === 0 ? (
|
||||
<p style={{ fontSize: 13, color: 'var(--text-muted)', textAlign: 'center', padding: 40 }}>加载中...</p>
|
||||
) : (
|
||||
<table style={s.table}>
|
||||
<div style={s.tableWrap}><table style={s.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={s.th}>用户名</th>
|
||||
|
|
@ -238,7 +239,7 @@ export default function AdminUsersPage() {
|
|||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</table></div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export default function RoleManager({ setResult }: Props) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<table style={ss.table}>
|
||||
<div style={{ overflowX: 'auto' }}><table style={ss.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={ss.th}>用户名</th><th style={ss.th}>显示名</th><th style={ss.th}>邮箱</th><th style={ss.th}>资产管理</th><th style={ss.th}>工单跟踪</th>
|
||||
|
|
@ -129,7 +129,7 @@ export default function RoleManager({ setResult }: Props) {
|
|||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</table></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,15 @@ export async function PATCH(request: Request) {
|
|||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// 同步更新 assets / issue 本地用户表
|
||||
const assetsDb = process.env.ASSETS_DB_PATH || '/Users/niuniu/programs/docker/assets-ai/data/assets.db'
|
||||
const issueDb = process.env.ISSUE_DB_PATH || '/Users/niuniu/programs/docker/issue-ai/data/issue.db'
|
||||
for (const dbPath of [assetsDb, issueDb]) {
|
||||
try {
|
||||
await execAsync(`sqlite3 "${dbPath}" "UPDATE users SET email = '${safeEmail}', updated_at = datetime('now', '+8 hours') WHERE username = '${safeUser}';"`, { timeout: 3000 })
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, username, email: email || '' })
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: '修改失败' }, { status: 500 })
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@ export async function PUT(request: Request) {
|
|||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
// 同步更新 assets / issue 本地用户表
|
||||
const assetsDb = process.env.ASSETS_DB_PATH || '/Users/niuniu/programs/docker/assets-ai/data/assets.db'
|
||||
const issueDb = process.env.ISSUE_DB_PATH || '/Users/niuniu/programs/docker/issue-ai/data/issue.db'
|
||||
for (const dbPath of [assetsDb, issueDb]) {
|
||||
try {
|
||||
await execAsync(`sqlite3 "${dbPath}" "UPDATE users SET email = '${safeEmail}', updated_at = datetime('now', '+8 hours') WHERE username = '${safeUser}';"`, { timeout: 3000 })
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, email: email || '' })
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : '修改失败'
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default function HeaderUI({ displayName, isAdmin, backLabel }: Props) {
|
|||
position: 'sticky', top: 0, zIndex: 50, height: 56,
|
||||
background: 'var(--bg-card)', borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<div style={{ height: '100%', padding: '0 40px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', maxWidth: 1440, margin: '0 auto' }}>
|
||||
<div className="header-inner" style={{ height: '100%', padding: '0 clamp(16px, 3vw, 48px)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
{/* 左侧 */}
|
||||
{backLabel ? (
|
||||
<a href="/" style={{ display: 'flex', alignItems: 'center', gap: 8, textDecoration: 'none', color: 'var(--text-secondary)', fontSize: 15 }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue