feat: add password reset for usermanager (#2688)
* feat: add password reset for usermanager - add a reset_key to account table - add ?reset_key= GET arguments to index.html and show reset form if provided - superuser can generate and copy reset url with key to share future ideas: - could add send forgot password email if user fill out email address * feat: simplify reset key * test: use reset key * test: add more tests * test: reset passwords do not match * test: `reset_password_auth_threshold_expired` --------- Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
parent
3a64cf5245
commit
a4c000d7dc
12 changed files with 344 additions and 10 deletions
|
|
@ -219,8 +219,8 @@ async def update_user_password(data: UpdateUserPassword, last_login_time: int) -
|
||||||
|
|
||||||
assert 0 <= time() - last_login_time <= settings.auth_credetials_update_threshold, (
|
assert 0 <= time() - last_login_time <= settings.auth_credetials_update_threshold, (
|
||||||
"You can only update your credentials in the first"
|
"You can only update your credentials in the first"
|
||||||
f" {settings.auth_credetials_update_threshold} seconds after login."
|
f" {settings.auth_credetials_update_threshold} seconds."
|
||||||
" Please login again!"
|
" Please login again or ask a new reset key!"
|
||||||
)
|
)
|
||||||
assert data.password == data.password_repeat, "Passwords do not match."
|
assert data.password == data.password_repeat, "Passwords do not match."
|
||||||
|
|
||||||
|
|
@ -240,7 +240,7 @@ async def update_user_password(data: UpdateUserPassword, last_login_time: int) -
|
||||||
)
|
)
|
||||||
|
|
||||||
user = await get_user(data.user_id)
|
user = await get_user(data.user_id)
|
||||||
assert user, "Updated account couldn't be retrieved"
|
assert user, "Updated account couldn't be retrieved."
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,12 @@ class UpdateUserPubkey(BaseModel):
|
||||||
pubkey: str = Query(default=..., max_length=64)
|
pubkey: str = Query(default=..., max_length=64)
|
||||||
|
|
||||||
|
|
||||||
|
class ResetUserPassword(BaseModel):
|
||||||
|
reset_key: str
|
||||||
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
|
||||||
|
|
||||||
class UpdateSuperuserPassword(BaseModel):
|
class UpdateSuperuserPassword(BaseModel):
|
||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,47 @@
|
||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
<q-card-section
|
||||||
|
v-if="authAction === 'reset' && authMethod === 'username-password'"
|
||||||
|
>
|
||||||
|
<b> <span v-text="$t('reset_password')"></span> </b><br /><br />
|
||||||
|
<q-form @submit="reset" class="q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
required
|
||||||
|
:disable="true"
|
||||||
|
v-model="reset_key"
|
||||||
|
:label="$t('reset_key') + ' *'"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="password"
|
||||||
|
:label="$t('password') + ' *'"
|
||||||
|
type="password"
|
||||||
|
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model="passwordRepeat"
|
||||||
|
:label="$t('password_repeat') + ' *'"
|
||||||
|
type="password"
|
||||||
|
:rules="[(val) => !val || val.length >= 8 || $t('invalid_password')]"
|
||||||
|
></q-input>
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
unelevated
|
||||||
|
color="primary"
|
||||||
|
:disable="!password || !passwordRepeat|| !reset_key || (password !== passwordRepeat)"
|
||||||
|
type="submit"
|
||||||
|
class="full-width"
|
||||||
|
:label="$t('reset_password')"
|
||||||
|
></q-btn>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card-section>
|
||||||
{%endif%} {% if LNBITS_NEW_ACCOUNTS_ALLOWED %}
|
{%endif%} {% if LNBITS_NEW_ACCOUNTS_ALLOWED %}
|
||||||
<q-card-section
|
<q-card-section
|
||||||
v-if="authAction === 'register' && authMethod === 'user-id-only'"
|
v-if="authAction === 'register' && authMethod === 'user-id-only'"
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,15 @@ include "users/_createWalletDialog.html" %}
|
||||||
>
|
>
|
||||||
<q-tooltip>Super User</q-tooltip>
|
<q-tooltip>Super User</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
icon="refresh"
|
||||||
|
size="sm"
|
||||||
|
color="secondary"
|
||||||
|
@click="resetPassword(props.row.id)"
|
||||||
|
>
|
||||||
|
<q-tooltip>Generate and copy password reset url</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
icon="delete"
|
icon="delete"
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ from ..models import (
|
||||||
CreateUser,
|
CreateUser,
|
||||||
LoginUsernamePassword,
|
LoginUsernamePassword,
|
||||||
LoginUsr,
|
LoginUsr,
|
||||||
|
ResetUserPassword,
|
||||||
UpdateSuperuserPassword,
|
UpdateSuperuserPassword,
|
||||||
UpdateUser,
|
UpdateUser,
|
||||||
UpdateUserPassword,
|
UpdateUserPassword,
|
||||||
|
|
@ -259,7 +260,50 @@ async def update_pubkey(
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(exc)
|
logger.debug(exc)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user password."
|
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user pubkey."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.put("/reset")
|
||||||
|
async def reset_password(data: ResetUserPassword) -> JSONResponse:
|
||||||
|
if not settings.is_auth_method_allowed(AuthMethods.username_and_password):
|
||||||
|
raise HTTPException(
|
||||||
|
HTTP_401_UNAUTHORIZED, "Auth by 'Username and Password' not allowed."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert data.reset_key[:10] == "reset_key_", "This is not a reset key."
|
||||||
|
|
||||||
|
reset_data_json = decrypt_internal_message(
|
||||||
|
base64.b64decode(data.reset_key[10:]).decode()
|
||||||
|
)
|
||||||
|
assert reset_data_json, "Cannot process reset key."
|
||||||
|
|
||||||
|
action, user_id, request_time = json.loads(reset_data_json)
|
||||||
|
assert action == "reset", "Expected reset action."
|
||||||
|
assert user_id is not None, "Missing user ID."
|
||||||
|
assert request_time is not None, "Missing reset time."
|
||||||
|
|
||||||
|
user = await get_account(user_id)
|
||||||
|
assert user, "User not found."
|
||||||
|
|
||||||
|
update_pwd = UpdateUserPassword(
|
||||||
|
user_id=user.id,
|
||||||
|
username=user.username or "",
|
||||||
|
password=data.password,
|
||||||
|
password_repeat=data.password_repeat,
|
||||||
|
)
|
||||||
|
user = await update_user_password(update_pwd, request_time)
|
||||||
|
|
||||||
|
return _auth_success_response(
|
||||||
|
username=user.username, user_id=user_id, email=user.email
|
||||||
|
)
|
||||||
|
except AssertionError as exc:
|
||||||
|
raise HTTPException(HTTP_403_FORBIDDEN, str(exc)) from exc
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(exc)
|
||||||
|
raise HTTPException(
|
||||||
|
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot reset user password."
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -309,7 +353,7 @@ async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug(exc)
|
logger.debug(exc)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot update user password."
|
HTTP_500_INTERNAL_SERVER_ERROR, "Cannot init user password."
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import time
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
@ -23,7 +26,7 @@ from lnbits.core.models import (
|
||||||
from lnbits.core.services import update_wallet_balance
|
from lnbits.core.services import update_wallet_balance
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
from lnbits.decorators import check_admin, check_super_user, parse_filters
|
||||||
from lnbits.helpers import generate_filter_params_openapi
|
from lnbits.helpers import encrypt_internal_message, generate_filter_params_openapi
|
||||||
from lnbits.settings import EditableSettings, settings
|
from lnbits.settings import EditableSettings, settings
|
||||||
|
|
||||||
users_router = APIRouter(prefix="/users/api/v1", dependencies=[Depends(check_admin)])
|
users_router = APIRouter(prefix="/users/api/v1", dependencies=[Depends(check_admin)])
|
||||||
|
|
@ -75,6 +78,24 @@ async def api_users_delete_user(
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.put(
|
||||||
|
"/user/{user_id}/reset_password", dependencies=[Depends(check_super_user)]
|
||||||
|
)
|
||||||
|
async def api_users_reset_password(user_id: str) -> str:
|
||||||
|
if user_id == settings.super_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="Cannot change superuser password.",
|
||||||
|
)
|
||||||
|
|
||||||
|
reset_data = ["reset", user_id, int(time.time())]
|
||||||
|
reset_data_json = json.dumps(reset_data, separators=(",", ":"), ensure_ascii=False)
|
||||||
|
reset_key = encrypt_internal_message(reset_data_json)
|
||||||
|
assert reset_key, "Cannot generate reset key."
|
||||||
|
reset_key_b64 = base64.b64encode(reset_key.encode()).decode()
|
||||||
|
return f"reset_key_{reset_key_b64}"
|
||||||
|
|
||||||
|
|
||||||
@users_router.get("/user/{user_id}/admin", dependencies=[Depends(check_super_user)])
|
@users_router.get("/user/{user_id}/admin", dependencies=[Depends(check_super_user)])
|
||||||
async def api_users_toggle_admin(user_id: str) -> None:
|
async def api_users_toggle_admin(user_id: str) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -266,5 +266,7 @@ window.localisation.en = {
|
||||||
hide_empty_wallets: 'Hide empty wallets',
|
hide_empty_wallets: 'Hide empty wallets',
|
||||||
recheck: 'Recheck',
|
recheck: 'Recheck',
|
||||||
contributors: 'Contributors',
|
contributors: 'Contributors',
|
||||||
license: 'License'
|
license: 'License',
|
||||||
|
reset_key: 'Reset Key',
|
||||||
|
reset_password: 'Reset Password'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,17 @@ window.LNbits = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
reset: function (reset_key, password, password_repeat) {
|
||||||
|
return axios({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/api/v1/auth/reset',
|
||||||
|
data: {
|
||||||
|
reset_key,
|
||||||
|
password,
|
||||||
|
password_repeat
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
login: function (username, password) {
|
login: function (username, password) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ window.app = Vue.createApp({
|
||||||
authMethod: 'username-password',
|
authMethod: 'username-password',
|
||||||
usr: '',
|
usr: '',
|
||||||
username: '',
|
username: '',
|
||||||
|
reset_key: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordRepeat: '',
|
passwordRepeat: '',
|
||||||
|
|
@ -128,6 +129,18 @@ window.app = Vue.createApp({
|
||||||
LNbits.utils.notifyApiError(e)
|
LNbits.utils.notifyApiError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
reset: async function () {
|
||||||
|
try {
|
||||||
|
await LNbits.api.reset(
|
||||||
|
this.reset_key,
|
||||||
|
this.password,
|
||||||
|
this.passwordRepeat
|
||||||
|
)
|
||||||
|
window.location.href = '/wallet'
|
||||||
|
} catch (e) {
|
||||||
|
LNbits.utils.notifyApiError(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
login: async function () {
|
login: async function () {
|
||||||
try {
|
try {
|
||||||
await LNbits.api.login(this.username, this.password)
|
await LNbits.api.login(this.username, this.password)
|
||||||
|
|
@ -171,5 +184,11 @@ window.app = Vue.createApp({
|
||||||
if (this.isUserAuthorized) {
|
if (this.isUserAuthorized) {
|
||||||
window.location.href = '/wallet'
|
window.location.href = '/wallet'
|
||||||
}
|
}
|
||||||
|
this.reset_key = new URLSearchParams(window.location.search).get(
|
||||||
|
'reset_key'
|
||||||
|
)
|
||||||
|
if (this.reset_key) {
|
||||||
|
this.authAction = 'reset'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,22 @@ window.app = Vue.createApp({
|
||||||
formatSat: function (value) {
|
formatSat: function (value) {
|
||||||
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
return LNbits.utils.formatSat(Math.floor(value / 1000))
|
||||||
},
|
},
|
||||||
|
resetPassword(user_id) {
|
||||||
|
return LNbits.api
|
||||||
|
.request('PUT', `/users/api/v1/user/${user_id}/reset_password`)
|
||||||
|
.then(res => {
|
||||||
|
this.$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'generated key for password reset',
|
||||||
|
icon: null
|
||||||
|
})
|
||||||
|
const url = window.location.origin + '?reset_key=' + res.data
|
||||||
|
this.copyText(url)
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
createUser() {
|
createUser() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('POST', '/users/api/v1/user', null, this.createUserDialog.data)
|
.request('POST', '/users/api/v1/user', null, this.createUserDialog.data)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import shortuuid
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
from lnbits.core.models import AccessTokenPayload, User
|
from lnbits.core.models import AccessTokenPayload, User
|
||||||
|
from lnbits.core.views.user_api import api_users_reset_password
|
||||||
from lnbits.settings import AuthMethods, settings
|
from lnbits.settings import AuthMethods, settings
|
||||||
from lnbits.utils.nostr import hex_to_npub, sign_event
|
from lnbits.utils.nostr import hex_to_npub, sign_event
|
||||||
|
|
||||||
|
|
@ -469,8 +470,8 @@ async def test_alan_change_password_auth_threshold_expired(
|
||||||
assert response.status_code == 403, "Treshold expired."
|
assert response.status_code == 403, "Treshold expired."
|
||||||
assert (
|
assert (
|
||||||
response.json().get("detail") == "You can only update your credentials"
|
response.json().get("detail") == "You can only update your credentials"
|
||||||
" in the first 1 seconds after login."
|
" in the first 1 seconds."
|
||||||
" Please login again!"
|
" Please login again or ask a new reset key!"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -856,3 +857,167 @@ async def test_alan_change_pubkey_auth_threshold_expired(
|
||||||
" in the first 1 seconds after login."
|
" in the first 1 seconds after login."
|
||||||
" Please login again!"
|
" Please login again!"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
################################ RESET PASSWORD ################################
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_request_reset_key_ok(http_client: AsyncClient):
|
||||||
|
tiny_id = shortuuid.uuid()[:8]
|
||||||
|
response = await http_client.post(
|
||||||
|
"/api/v1/auth/register",
|
||||||
|
json={
|
||||||
|
"username": f"u21.{tiny_id}",
|
||||||
|
"password": "secret1234",
|
||||||
|
"password_repeat": "secret1234",
|
||||||
|
"email": f"u21.{tiny_id}@lnbits.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, "User created."
|
||||||
|
access_token = response.json().get("access_token")
|
||||||
|
assert access_token is not None
|
||||||
|
|
||||||
|
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||||
|
access_token_payload = AccessTokenPayload(**payload)
|
||||||
|
assert access_token_payload.usr, "User id set."
|
||||||
|
|
||||||
|
reset_key = await api_users_reset_password(access_token_payload.usr)
|
||||||
|
assert reset_key, "Reset key created."
|
||||||
|
assert reset_key[:10] == "reset_key_", "This is not a reset key."
|
||||||
|
|
||||||
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/reset",
|
||||||
|
json={
|
||||||
|
"reset_key": reset_key,
|
||||||
|
"password": "secret0000",
|
||||||
|
"password_repeat": "secret0000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, "Password reset."
|
||||||
|
access_token = response.json().get("access_token")
|
||||||
|
assert access_token is not None
|
||||||
|
|
||||||
|
response = await http_client.post(
|
||||||
|
"/api/v1/auth", json={"username": f"u21.{tiny_id}", "password": "secret1234"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 401, "Old passord not valid."
|
||||||
|
assert response.json().get("detail") == "Invalid credentials."
|
||||||
|
|
||||||
|
response = await http_client.post(
|
||||||
|
"/api/v1/auth", json={"username": f"u21.{tiny_id}", "password": "secret0000"}
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, "Login new password OK."
|
||||||
|
access_token = response.json().get("access_token")
|
||||||
|
assert access_token is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_request_reset_key_user_not_found(http_client: AsyncClient):
|
||||||
|
user_id = "926abb2ab59a48ebb2485bcceb58d05e"
|
||||||
|
reset_key = await api_users_reset_password(user_id)
|
||||||
|
assert reset_key, "Reset key created."
|
||||||
|
assert reset_key[:10] == "reset_key_", "This is not a reset key."
|
||||||
|
|
||||||
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/reset",
|
||||||
|
json={
|
||||||
|
"reset_key": reset_key,
|
||||||
|
"password": "secret0000",
|
||||||
|
"password_repeat": "secret0000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403, "User does not exist."
|
||||||
|
assert response.json().get("detail") == "User not found."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reset_username_password_not_allowed(http_client: AsyncClient):
|
||||||
|
# exclude 'username_password'
|
||||||
|
settings.auth_allowed_methods = [AuthMethods.user_id_only.value]
|
||||||
|
|
||||||
|
user_id = "926abb2ab59a48ebb2485bcceb58d05e"
|
||||||
|
reset_key = await api_users_reset_password(user_id)
|
||||||
|
assert reset_key, "Reset key created."
|
||||||
|
|
||||||
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/reset",
|
||||||
|
json={
|
||||||
|
"reset_key": reset_key,
|
||||||
|
"password": "secret0000",
|
||||||
|
"password_repeat": "secret0000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
settings.auth_allowed_methods = AuthMethods.all()
|
||||||
|
|
||||||
|
assert response.status_code == 401, "Login method not allowed."
|
||||||
|
assert (
|
||||||
|
response.json().get("detail") == "Auth by 'Username and Password' not allowed."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reset_username_passwords_do_not_matcj(
|
||||||
|
http_client: AsyncClient, user_alan: User
|
||||||
|
):
|
||||||
|
|
||||||
|
reset_key = await api_users_reset_password(user_alan.id)
|
||||||
|
assert reset_key, "Reset key created."
|
||||||
|
|
||||||
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/reset",
|
||||||
|
json={
|
||||||
|
"reset_key": reset_key,
|
||||||
|
"password": "secret0000",
|
||||||
|
"password_repeat": "secret-does-not-mathc",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403, "Passwords do not match."
|
||||||
|
assert response.json().get("detail") == "Passwords do not match."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reset_username_password_bad_key(http_client: AsyncClient):
|
||||||
|
|
||||||
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/reset",
|
||||||
|
json={
|
||||||
|
"reset_key": "reset_key_xxxxxxxxxxx",
|
||||||
|
"password": "secret0000",
|
||||||
|
"password_repeat": "secret0000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 500, "Bad reset key."
|
||||||
|
assert response.json().get("detail") == "Cannot reset user password."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reset_password_auth_threshold_expired(
|
||||||
|
user_alan: User, http_client: AsyncClient
|
||||||
|
):
|
||||||
|
|
||||||
|
reset_key = await api_users_reset_password(user_alan.id)
|
||||||
|
assert reset_key, "Reset key created."
|
||||||
|
|
||||||
|
initial_update_threshold = settings.auth_credetials_update_threshold
|
||||||
|
settings.auth_credetials_update_threshold = 1
|
||||||
|
time.sleep(1.1)
|
||||||
|
response = await http_client.put(
|
||||||
|
"/api/v1/auth/reset",
|
||||||
|
json={
|
||||||
|
"reset_key": reset_key,
|
||||||
|
"password": "secret0000",
|
||||||
|
"password_repeat": "secret0000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
settings.auth_credetials_update_threshold = initial_update_threshold
|
||||||
|
|
||||||
|
assert response.status_code == 403, "Treshold expired."
|
||||||
|
assert (
|
||||||
|
response.json().get("detail") == "You can only update your credentials"
|
||||||
|
" in the first 1 seconds."
|
||||||
|
" Please login again or ask a new reset key!"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue