feat(auth,prompt-rule): add password change with session revocation and scope prompt rules by brand
Add self-service password change that re-hashes the credential and bumps a per-user session version so all existing access/refresh tokens are rejected. Session version is tracked in Redis with an in-memory fallback and enforced in middleware and refresh. Scope prompt rules and rule groups to a brand: new brand_id column (migrated to a "未归属" fallback brand for existing rows), brand-filtered queries/indexes, and brand-aware admin-web tabs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import type {
|
||||
Brand,
|
||||
BrandLibrarySummary,
|
||||
BrandRequest,
|
||||
ChangePasswordRequest,
|
||||
Competitor,
|
||||
CompetitorRequest,
|
||||
ComplianceAckRecord,
|
||||
@@ -322,6 +323,7 @@ const currentBrandScopedPathPatterns = [
|
||||
/^\/api\/tenant\/instant-tasks(?:\/|$)/,
|
||||
/^\/api\/tenant\/publish-jobs(?:\/|$)/,
|
||||
/^\/api\/tenant\/publish-tasks\/[^/]+\/retry(?:\/|$)/,
|
||||
/^\/api\/tenant\/prompt-rules(?:\/|$)/,
|
||||
]
|
||||
|
||||
function shouldAttachCurrentBrandHeader(url: unknown): boolean {
|
||||
@@ -406,6 +408,9 @@ export const authApi = {
|
||||
me() {
|
||||
return apiClient.get<UserInfo>('/api/auth/me')
|
||||
},
|
||||
changePassword(payload: ChangePasswordRequest) {
|
||||
return apiClient.post<{ ok: boolean }, ChangePasswordRequest>('/api/auth/password', payload)
|
||||
},
|
||||
logout() {
|
||||
return apiClient.post<null>('/api/auth/logout')
|
||||
},
|
||||
|
||||
@@ -15,6 +15,8 @@ const authSessionErrorMessages = new Set([
|
||||
|
||||
const errorMessageMap: Record<string, string> = {
|
||||
invalid_credentials: '邮箱或密码错误',
|
||||
invalid_old_password: '原密码错误',
|
||||
weak_password: '新密码至少 8 位',
|
||||
login_rate_limited: '登录请求过于频繁',
|
||||
login_locked: '登录已被临时保护',
|
||||
login_in_progress: '登录请求正在处理中',
|
||||
|
||||
@@ -10,16 +10,23 @@ export async function encryptPasswordForLogin(
|
||||
password: string,
|
||||
key: PasswordPublicKeyResponse,
|
||||
): Promise<{ encrypted_password: string; password_key_id: string }> {
|
||||
return {
|
||||
encrypted_password: await encryptPassword(password, key),
|
||||
password_key_id: key.key_id,
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptPassword(
|
||||
password: string,
|
||||
key: PasswordPublicKeyResponse,
|
||||
): Promise<string> {
|
||||
const cryptoKey = await importRSAOAEPKey(key.public_key)
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: 'RSA-OAEP' },
|
||||
cryptoKey,
|
||||
textEncoder.encode(password),
|
||||
)
|
||||
return {
|
||||
encrypted_password: arrayBufferToBase64(encrypted),
|
||||
password_key_id: key.key_id,
|
||||
}
|
||||
return arrayBufferToBase64(encrypted)
|
||||
}
|
||||
|
||||
async function importRSAOAEPKey(publicKeyPEM: string): Promise<CryptoKey> {
|
||||
|
||||
Reference in New Issue
Block a user