feat: revoke other login sessions
This commit is contained in:
@@ -272,6 +272,18 @@ async def list_sessions(
|
|||||||
return [{"id": row.id, "current": row.token_hash == current_hash, "created_at": row.created_at, "last_seen_at": row.last_seen_at, "expires_at": row.expires_at, "ip_address": row.ip_address, "user_agent": row.user_agent} for row in rows]
|
return [{"id": row.id, "current": row.token_hash == current_hash, "created_at": row.created_at, "last_seen_at": row.last_seen_at, "expires_at": row.expires_at, "ip_address": row.ip_address, "user_agent": row.user_agent} for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/v1/sessions/others", status_code=204)
|
||||||
|
async def revoke_other_sessions(
|
||||||
|
token: str = Depends(session_token),
|
||||||
|
user: User = Depends(current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
current_hash = hash_token(token)
|
||||||
|
await db.execute(delete(Session).where(Session.user_id == user.id, Session.token_hash != current_hash))
|
||||||
|
await db.commit()
|
||||||
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/v1/sessions/{session_id}", status_code=204)
|
@app.delete("/api/v1/sessions/{session_id}", status_code=204)
|
||||||
async def revoke_session(
|
async def revoke_session(
|
||||||
session_id: UUID,
|
session_id: UUID,
|
||||||
|
|||||||
@@ -511,6 +511,14 @@ async function loadSettings() {
|
|||||||
async function revoke(id: string) {
|
async function revoke(id: string) {
|
||||||
await safe(async () => { await request(`/sessions/${id}`, { method: 'DELETE' }); await loadSettings(); emit('notice', '会话已撤销') })
|
await safe(async () => { await request(`/sessions/${id}`, { method: 'DELETE' }); await loadSettings(); emit('notice', '会话已撤销') })
|
||||||
}
|
}
|
||||||
|
async function revokeOtherSessions() {
|
||||||
|
if (!confirm('撤销其他所有设备的登录会话?当前设备会保持登录。')) return
|
||||||
|
await safe(async () => {
|
||||||
|
await request('/sessions/others', { method: 'DELETE' })
|
||||||
|
await loadSettings()
|
||||||
|
emit('notice', '其他设备的登录会话已全部撤销')
|
||||||
|
})
|
||||||
|
}
|
||||||
function downloadBlob(blob: Blob, name: string) {
|
function downloadBlob(blob: Blob, name: string) {
|
||||||
const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000)
|
const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||||
}
|
}
|
||||||
@@ -669,7 +677,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
<article class="tool-card"><FileJson /><h2>数据导出与恢复</h2><p>导出完整 CSV 数据,或从 CSV / JSON 备份恢复。</p><button class="soft-button" @click="exportData"><Download />导出 CSV</button><label class="file-button"><ArchiveRestore />选择备份<input type="file" accept=".csv,application/json" @change="restoreFile=($event.target as HTMLInputElement).files?.[0]||null"></label><button v-if="restoreFile" class="danger-button" @click="restore">确认恢复</button></article>
|
<article class="tool-card"><FileJson /><h2>数据导出与恢复</h2><p>导出完整 CSV 数据,或从 CSV / JSON 备份恢复。</p><button class="soft-button" @click="exportData"><Download />导出 CSV</button><label class="file-button"><ArchiveRestore />选择备份<input type="file" accept=".csv,application/json" @change="restoreFile=($event.target as HTMLInputElement).files?.[0]||null"></label><button v-if="restoreFile" class="danger-button" @click="restore">确认恢复</button></article>
|
||||||
<article class="tool-card password-card"><Activity /><h2>修改密码</h2><p>修改后当前设备保持登录,其他设备会自动退出。</p><form class="password-form" @submit.prevent="changePassword"><label>当前密码<input v-model="currentPassword" type="password" autocomplete="current-password" required aria-label="当前密码"></label><label>新密码<input v-model="newPassword" type="password" autocomplete="new-password" minlength="12" required aria-label="新密码" placeholder="至少 12 位"></label><label>确认新密码<input v-model="confirmPassword" type="password" autocomplete="new-password" minlength="12" required aria-label="确认新密码"></label><p v-if="passwordError" class="inline-error" role="alert">{{passwordError}}</p><button class="primary-small" :disabled="passwordBusy || !currentPassword || !newPassword || !confirmPassword">{{passwordBusy?'正在修改…':'修改密码'}}</button></form></article>
|
<article class="tool-card password-card"><Activity /><h2>修改密码</h2><p>修改后当前设备保持登录,其他设备会自动退出。</p><form class="password-form" @submit.prevent="changePassword"><label>当前密码<input v-model="currentPassword" type="password" autocomplete="current-password" required aria-label="当前密码"></label><label>新密码<input v-model="newPassword" type="password" autocomplete="new-password" minlength="12" required aria-label="新密码" placeholder="至少 12 位"></label><label>确认新密码<input v-model="confirmPassword" type="password" autocomplete="new-password" minlength="12" required aria-label="确认新密码"></label><p v-if="passwordError" class="inline-error" role="alert">{{passwordError}}</p><button class="primary-small" :disabled="passwordBusy || !currentPassword || !newPassword || !confirmPassword">{{passwordBusy?'正在修改…':'修改密码'}}</button></form></article>
|
||||||
<article class="tool-card wide"><LogOut /><h2>登录会话</h2><div v-for="s in sessions" :key="s.id" class="session-row"><span class="session-copy"><b class="session-title">{{ s.current ? '当前设备' : '其他设备' }}</b><small class="session-meta"><span class="session-device">{{ formatUserAgent(s.user_agent) }}</span><span aria-hidden="true"> · </span><time :datetime="s.last_seen_at ?? s.created_at">{{ formatLocalShortDateTime(s.last_seen_at ?? s.created_at) }}</time></small></span><button v-if="!s.current" class="danger-text session-revoke" :aria-label="`撤销 ${formatUserAgent(s.user_agent)} 的登录会话`" @click="revoke(s.id)">撤销</button></div><p v-if="!sessions.length">没有可显示的会话。</p></article>
|
<article class="tool-card wide"><LogOut /><h2>登录会话</h2><div class="session-card-actions"><p>可单独撤销设备,也可以一次撤销除当前设备外的全部会话。</p><button v-if="sessions.some((s) => !s.current)" type="button" class="danger-text session-revoke-all" :disabled="busy" @click="revokeOtherSessions">撤销其他所有会话</button></div><div v-for="s in sessions" :key="s.id" class="session-row"><span class="session-copy"><b class="session-title">{{ s.current ? '当前设备' : '其他设备' }}</b><small class="session-meta"><span class="session-device">{{ formatUserAgent(s.user_agent) }}</span><span aria-hidden="true"> · </span><time :datetime="s.last_seen_at ?? s.created_at">{{ formatLocalShortDateTime(s.last_seen_at ?? s.created_at) }}</time></small></span><button v-if="!s.current" class="danger-text session-revoke" :aria-label="`撤销 ${formatUserAgent(s.user_agent)} 的登录会话`" @click="revoke(s.id)">撤销</button></div><p v-if="!sessions.length">没有可显示的会话。</p></article>
|
||||||
<article v-if="audit.length" class="tool-card wide"><Activity /><h2>最近活动</h2><div v-for="(row, i) in audit" :key="row.id || i" class="audit-row"><div class="audit-copy"><span class="audit-action" :title="row.action ?? row.event ?? undefined">{{ formatAuditAction(row.action ?? row.event) }}{{ formatAuditAction(row.action ?? row.event) === '其他操作' ? '' : formatAuditEntity(row.entity_type) }}</span><time :datetime="row.created_at ?? row.timestamp">{{ formatLocalShortDateTime(row.created_at ?? row.timestamp) }}</time></div></div></article>
|
<article v-if="audit.length" class="tool-card wide"><Activity /><h2>最近活动</h2><div v-for="(row, i) in audit" :key="row.id || i" class="audit-row"><div class="audit-copy"><span class="audit-action" :title="row.action ?? row.event ?? undefined">{{ formatAuditAction(row.action ?? row.event) }}{{ formatAuditAction(row.action ?? row.event) === '其他操作' ? '' : formatAuditEntity(row.entity_type) }}</span><time :datetime="row.created_at ?? row.timestamp">{{ formatLocalShortDateTime(row.created_at ?? row.timestamp) }}</time></div></div></article>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ main{container-type:inline-size;min-width:0;padding:27px 34px 50px;overflow:auto
|
|||||||
.habit-row>.icon.ghost{width:44px;height:44px;flex:0 0 44px}.habit-check{margin-left:-4px}.habit-row.done .habit-check .task-check-mark{background:#71856b;border-color:#71856b;color:#fff}.habit-detail-mask{position:fixed;z-index:85;inset:0;background:rgba(45,38,31,.36);display:grid;place-items:end center;padding:20px}.habit-detail-sheet{width:min(500px,100%);background:#fffdf8;border:1px solid var(--line);border-radius:20px;box-shadow:0 12px 36px rgba(56,40,24,.18);padding:18px;display:grid;gap:16px}.habit-detail-sheet header{display:flex;align-items:center;justify-content:space-between;gap:10px}.habit-detail-sheet header small{color:var(--muted);font-size:11px}.habit-detail-sheet header h3{margin:2px 0 0;font-size:19px}.habit-detail-sheet header button{width:44px;height:44px;border:0;background:transparent;display:grid;place-items:center}.habit-detail-progress{display:flex;justify-content:space-between;align-items:center;padding:14px 0;border-block:1px solid var(--line);font-size:12px;color:var(--muted)}.habit-detail-progress strong{font-size:14px;color:#3c372f}.habit-detail-sheet footer{display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}.habit-detail-sheet .habit-delete-button{font-weight:750}
|
.habit-row>.icon.ghost{width:44px;height:44px;flex:0 0 44px}.habit-check{margin-left:-4px}.habit-row.done .habit-check .task-check-mark{background:#71856b;border-color:#71856b;color:#fff}.habit-detail-mask{position:fixed;z-index:85;inset:0;background:rgba(45,38,31,.36);display:grid;place-items:end center;padding:20px}.habit-detail-sheet{width:min(500px,100%);background:#fffdf8;border:1px solid var(--line);border-radius:20px;box-shadow:0 12px 36px rgba(56,40,24,.18);padding:18px;display:grid;gap:16px}.habit-detail-sheet header{display:flex;align-items:center;justify-content:space-between;gap:10px}.habit-detail-sheet header small{color:var(--muted);font-size:11px}.habit-detail-sheet header h3{margin:2px 0 0;font-size:19px}.habit-detail-sheet header button{width:44px;height:44px;border:0;background:transparent;display:grid;place-items:center}.habit-detail-progress{display:flex;justify-content:space-between;align-items:center;padding:14px 0;border-block:1px solid var(--line);font-size:12px;color:var(--muted)}.habit-detail-progress strong{font-size:14px;color:#3c372f}.habit-detail-sheet footer{display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}.habit-detail-sheet .habit-delete-button{font-weight:750}
|
||||||
.empty-panel{text-align:center;color:var(--muted);display:grid;place-items:center;gap:10px}.today-empty-panel{min-height:130px}.empty-action{margin-top:4px;color:#655d52}.empty-action svg{width:15px;height:15px}
|
.empty-panel{text-align:center;color:var(--muted);display:grid;place-items:center;gap:10px}.today-empty-panel{min-height:130px}.empty-action{margin-top:4px;color:#655d52}.empty-action svg{width:15px;height:15px}
|
||||||
.field-error{display:block;color:var(--danger);font-size:12px;line-height:1.45;overflow-wrap:anywhere}.habit-state-note{color:var(--muted);font-size:11px}.habit-weekdays{min-width:0;border:0;padding:0;display:flex;flex-wrap:wrap;gap:8px}.habit-weekdays legend{width:100%;font-size:12px;font-weight:650}.habit-weekdays label{min-height:44px;display:flex;align-items:center;gap:4px}.habit-compose-sheet input,.habit-compose-sheet select{max-width:100%}.task-check:disabled{cursor:not-allowed;opacity:.55}
|
.field-error{display:block;color:var(--danger);font-size:12px;line-height:1.45;overflow-wrap:anywhere}.habit-state-note{color:var(--muted);font-size:11px}.habit-weekdays{min-width:0;border:0;padding:0;display:flex;flex-wrap:wrap;gap:8px}.habit-weekdays legend{width:100%;font-size:12px;font-weight:650}.habit-weekdays label{min-height:44px;display:flex;align-items:center;gap:4px}.habit-compose-sheet input,.habit-compose-sheet select{max-width:100%}.task-check:disabled{cursor:not-allowed;opacity:.55}
|
||||||
.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.tool-card{display:flex;flex-direction:column;align-items:flex-start;gap:10px}.tool-card>h2{margin:0;font-size:1.17em}.tool-card>svg{color:var(--accent);width:25px;height:25px}.tool-card p{margin:0;color:var(--muted);font-size:13px}.tool-card.wide{grid-column:1/-1}.password-form{width:100%;display:grid;gap:10px}.password-form label{display:grid;gap:6px;color:#675f54;font-size:12px;font-weight:650}.password-form input{width:100%;border:1px solid var(--line);background:#fff;border-radius:10px;padding:11px 12px;outline:none}.password-form input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(241,90,41,.1)}.password-form button{justify-self:start}.file-button input{display:none}.tool-card pre{width:100%;max-height:180px;overflow:auto;background:#f8f3e8;padding:10px;border-radius:8px;font-size:10px}.session-row,.audit-row{width:100%;display:flex;justify-content:space-between;align-items:center;gap:10px;border-top:1px solid var(--line);padding:9px 0}.session-copy{min-width:0;flex:1;display:grid}.session-title{line-height:1.4}.session-meta{min-width:0;display:flex;flex-wrap:wrap;align-items:center;line-height:1.45}.session-device{min-width:0;overflow-wrap:anywhere}.session-revoke{min-width:44px;min-height:44px;flex:0 0 auto;justify-content:center}.audit-copy{min-width:0;display:grid;gap:2px}.audit-action{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:anywhere}.session-row small,.audit-row time{color:var(--muted);font-size:11px}.inline-error{padding:9px;border-radius:8px;color:var(--danger);background:#fff0ed}.settings.active{background:var(--accent-soft);color:#b7421e;font-weight:700}
|
.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.tool-card{display:flex;flex-direction:column;align-items:flex-start;gap:10px}.tool-card>h2{margin:0;font-size:1.17em}.tool-card>svg{color:var(--accent);width:25px;height:25px}.tool-card p{margin:0;color:var(--muted);font-size:13px}.tool-card.wide{grid-column:1/-1}.session-card-actions{width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px}.session-card-actions p{min-width:0}.session-revoke-all{min-height:44px;flex:0 0 auto}.password-form{width:100%;display:grid;gap:10px}.password-form label{display:grid;gap:6px;color:#675f54;font-size:12px;font-weight:650}.password-form input{width:100%;border:1px solid var(--line);background:#fff;border-radius:10px;padding:11px 12px;outline:none}.password-form input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(241,90,41,.1)}.password-form button{justify-self:start}.file-button input{display:none}.tool-card pre{width:100%;max-height:180px;overflow:auto;background:#f8f3e8;padding:10px;border-radius:8px;font-size:10px}.session-row,.audit-row{width:100%;display:flex;justify-content:space-between;align-items:center;gap:10px;border-top:1px solid var(--line);padding:9px 0}.session-copy{min-width:0;flex:1;display:grid}.session-title{line-height:1.4}.session-meta{min-width:0;display:flex;flex-wrap:wrap;align-items:center;line-height:1.45}.session-device{min-width:0;overflow-wrap:anywhere}.session-revoke{min-width:44px;min-height:44px;flex:0 0 auto;justify-content:center}.audit-copy{min-width:0;display:grid;gap:2px}.audit-action{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:anywhere}.session-row small,.audit-row time{color:var(--muted);font-size:11px}.inline-error{padding:9px;border-radius:8px;color:var(--danger);background:#fff0ed}.settings.active{background:var(--accent-soft);color:#b7421e;font-weight:700}
|
||||||
@media(max-width:930px){.task-row,.habit-row,.countdown-row{min-height:62px;background:#fff;border:1px solid var(--line);border-radius:13px;box-shadow:none}.task-list,.habit-list,.countdown-timeline{display:grid;gap:8px}.task-row{padding:4px 7px}.task-row:hover,.task-row.selected{background:#fffaf5}.habit-row{padding:4px 7px}.countdown-row,.countdown-row:first-of-type{border:1px solid var(--line)}.countdown-row{grid-template-columns:minmax(0,1fr) 72px;padding:8px 9px;gap:8px}.task-main strong,.habit-name,.countdown-main>b{font-size:14px;font-weight:650}.meta,.countdown-main>small,.countdown-state small{font-size:11px;color:var(--muted)}.habit-progress{font-size:16px}.task-check{width:44px;flex-basis:44px}.countdown-icon{width:36px;height:36px;border-radius:10px}.countdown-state strong{font-size:24px}.countdown-group{gap:8px}.countdown-group>h3{padding-left:3px}.habit-detail-mask{padding:0}.habit-detail-sheet{width:100%;border-radius:22px 22px 0 0;padding:18px 16px calc(18px + env(safe-area-inset-bottom))}}
|
@media(max-width:930px){.task-row,.habit-row,.countdown-row{min-height:62px;background:#fff;border:1px solid var(--line);border-radius:13px;box-shadow:none}.task-list,.habit-list,.countdown-timeline{display:grid;gap:8px}.task-row{padding:4px 7px}.task-row:hover,.task-row.selected{background:#fffaf5}.habit-row{padding:4px 7px}.countdown-row,.countdown-row:first-of-type{border:1px solid var(--line)}.countdown-row{grid-template-columns:minmax(0,1fr) 72px;padding:8px 9px;gap:8px}.task-main strong,.habit-name,.countdown-main>b{font-size:14px;font-weight:650}.meta,.countdown-main>small,.countdown-state small{font-size:11px;color:var(--muted)}.habit-progress{font-size:16px}.task-check{width:44px;flex-basis:44px}.countdown-icon{width:36px;height:36px;border-radius:10px}.countdown-state strong{font-size:24px}.countdown-group{gap:8px}.countdown-group>h3{padding-left:3px}.habit-detail-mask{padding:0}.habit-detail-sheet{width:100%;border-radius:22px 22px 0 0;padding:18px 16px calc(18px + env(safe-area-inset-bottom))}}
|
||||||
@media(max-width:800px){.settings-grid{grid-template-columns:1fr}.tool-card.wide{grid-column:auto}.habit-main{padding:10px 2px}.numeric-action input{width:62px}}
|
@media(max-width:800px){.settings-grid{grid-template-columns:1fr}.tool-card.wide{grid-column:auto}.habit-main{padding:10px 2px}.numeric-action input{width:62px}}
|
||||||
@media(max-width:390px){.task-compose-sheet{width:100%;max-width:100%;overflow-x:hidden}.task-compose-sheet .app-sheet__header>button{min-width:44px;min-height:44px}.task-compose-sheet input,.task-compose-sheet select,.task-compose-sheet button{min-height:44px}.task-compose-date-clear,.task-compose-time-remove{min-width:44px;min-height:44px}.task-compose-sheet .app-sheet__footer>button{min-height:44px}.habit-detail-sheet,.habit-compose-sheet{width:100%;max-width:100%}.habit-detail-sheet .app-sheet__header>button,.habit-compose-sheet .app-sheet__header>button{min-width:44px;min-height:44px}.habit-detail-sheet .app-sheet__footer>button,.habit-detail-sheet .app-sheet__danger>button,.habit-compose-sheet .app-sheet__footer>button{min-height:44px}}
|
@media(max-width:390px){.task-compose-sheet{width:100%;max-width:100%;overflow-x:hidden}.task-compose-sheet .app-sheet__header>button{min-width:44px;min-height:44px}.task-compose-sheet input,.task-compose-sheet select,.task-compose-sheet button{min-height:44px}.task-compose-date-clear,.task-compose-time-remove{min-width:44px;min-height:44px}.task-compose-sheet .app-sheet__footer>button{min-height:44px}.habit-detail-sheet,.habit-compose-sheet{width:100%;max-width:100%}.habit-detail-sheet .app-sheet__header>button,.habit-compose-sheet .app-sheet__header>button{min-width:44px;min-height:44px}.habit-detail-sheet .app-sheet__footer>button,.habit-detail-sheet .app-sheet__danger>button,.habit-compose-sheet .app-sheet__footer>button{min-height:44px}}
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ describe('settings sessions and audit activity', () => {
|
|||||||
expect(mvpPanel).toContain(':datetime="s.last_seen_at ?? s.created_at"')
|
expect(mvpPanel).toContain(':datetime="s.last_seen_at ?? s.created_at"')
|
||||||
expect(mvpPanel).toContain(':aria-label="`撤销 ${formatUserAgent(s.user_agent)} 的登录会话`"')
|
expect(mvpPanel).toContain(':aria-label="`撤销 ${formatUserAgent(s.user_agent)} 的登录会话`"')
|
||||||
expect(mvpPanel).toContain('class="danger-text session-revoke"')
|
expect(mvpPanel).toContain('class="danger-text session-revoke"')
|
||||||
|
expect(mvpPanel).toContain('class="session-card-actions"')
|
||||||
|
expect(mvpPanel).toContain('撤销其他所有会话')
|
||||||
|
expect(mvpPanel).toContain("request('/sessions/others', { method: 'DELETE' })")
|
||||||
|
expect(mvpPanel).toContain("confirm('撤销其他所有设备的登录会话?当前设备会保持登录。')")
|
||||||
|
expect(css).toContain('.session-card-actions{')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not expose the raw user agent in session DOM attributes', () => {
|
it('does not expose the raw user agent in session DOM attributes', () => {
|
||||||
|
|||||||
@@ -174,6 +174,30 @@ def test_logout_revokes_current_session(client):
|
|||||||
assert client.get("/api/v1/me").status_code == 401
|
assert client.get("/api/v1/me").status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_revoke_other_sessions_keeps_current_session(client):
|
||||||
|
password = "correct horse battery staple"
|
||||||
|
client.post("/api/v1/setup/initialize", json={"username": "owner", "password": password})
|
||||||
|
other = type(client)(client.app)
|
||||||
|
third = type(client)(client.app)
|
||||||
|
try:
|
||||||
|
assert other.post("/api/v1/auth/login", json={"username": "owner", "password": password}).status_code == 200
|
||||||
|
assert third.post("/api/v1/auth/login", json={"username": "owner", "password": password}).status_code == 200
|
||||||
|
assert len(client.get("/api/v1/sessions").json()) == 3
|
||||||
|
|
||||||
|
assert client.delete("/api/v1/sessions/others").status_code == 204
|
||||||
|
|
||||||
|
remaining = client.get("/api/v1/sessions")
|
||||||
|
assert remaining.status_code == 200
|
||||||
|
assert len(remaining.json()) == 1
|
||||||
|
assert remaining.json()[0]["current"] is True
|
||||||
|
assert client.get("/api/v1/me").status_code == 200
|
||||||
|
assert other.get("/api/v1/me").status_code == 401
|
||||||
|
assert third.get("/api/v1/me").status_code == 401
|
||||||
|
finally:
|
||||||
|
other.close()
|
||||||
|
third.close()
|
||||||
|
|
||||||
|
|
||||||
def test_change_password_checks_current_password_and_revokes_other_sessions(client):
|
def test_change_password_checks_current_password_and_revokes_other_sessions(client):
|
||||||
old_password = "correct horse battery staple"
|
old_password = "correct horse battery staple"
|
||||||
new_password = "new correct horse battery staple"
|
new_password = "new correct horse battery staple"
|
||||||
|
|||||||
Reference in New Issue
Block a user