Add email sending (service + 3 flows) and a general asset label generator
Email: a stdlib SMTP mailer (settings-first config, graceful no-op when unconfigured), a test-email endpoint wired to the Email settings page, forced first-login password change (users.mustchangepassword, migration 7d23, /change-password flow), new-user welcome mail, and on-demand report/alert delivery (POST /api/reports/email + Email Report buttons) with an external-cron-with-a-scoped-PAT path documented for automation. All tests patch smtplib - no network. Labels: a shared /print/asset-label/<type>/<id> view any asset detail page opens - card or plain style, QR or barcode, configurable encoding. Per-type qr_target_* templates plus label_default_style/codetype/encodes settings on the Printing page. Measuring-tool labels default to encoding their inspection-operation code (derived from the location name, e.g. 0615), so every tool in an area shares the area code - verified by decoding the rendered QR. Machine labels default to the machine number; blank-serial handled gracefully. 808 tests pass; both features verified live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ from flask_jwt_extended import (
|
||||
get_jwt_identity,
|
||||
current_user
|
||||
)
|
||||
from werkzeug.security import check_password_hash
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
from shopdb.extensions import db, cache
|
||||
from shopdb.core.models import User
|
||||
@@ -151,7 +151,8 @@ def login():
|
||||
'email': user.email,
|
||||
'firstname': user.firstname,
|
||||
'lastname': user.lastname,
|
||||
'roles': [r.rolename for r in user.roles]
|
||||
'roles': [r.rolename for r in user.roles],
|
||||
'mustchangepassword': bool(user.mustchangepassword)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -196,10 +197,59 @@ def get_current_user():
|
||||
'firstname': current_user.firstname,
|
||||
'lastname': current_user.lastname,
|
||||
'roles': [r.rolename for r in current_user.roles],
|
||||
'permissions': current_user.getpermissions()
|
||||
'permissions': current_user.getpermissions(),
|
||||
'mustchangepassword': bool(current_user.mustchangepassword)
|
||||
})
|
||||
|
||||
|
||||
@auth_bp.route('/change-password', methods=['POST'])
|
||||
@jwt_required()
|
||||
def change_password():
|
||||
"""Change the authenticated user's own password.
|
||||
|
||||
Request:
|
||||
{ "current_password": "string", "new_password": "string" }
|
||||
|
||||
current_password is required for a normal self-service change. When the
|
||||
account is flagged mustchangepassword (an admin set a temporary password),
|
||||
the forced-change case accepts new_password alone. On success the flag is
|
||||
cleared and any lockout/failed-login state is reset.
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
new_password = data.get('new_password')
|
||||
current_password = data.get('current_password')
|
||||
|
||||
if not new_password:
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR, 'new_password is required')
|
||||
if len(new_password) < 8:
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'New password must be at least 8 characters')
|
||||
|
||||
user = current_user
|
||||
|
||||
# A normal change must prove knowledge of the current password. The forced
|
||||
# first-login case (admin-set temp password) may skip it.
|
||||
if not user.mustchangepassword:
|
||||
if not current_password:
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR, 'current_password is required')
|
||||
if not check_password_hash(user.passwordhash, current_password):
|
||||
return error_response(
|
||||
ErrorCodes.UNAUTHORIZED,
|
||||
'Current password is incorrect', http_code=401)
|
||||
|
||||
user.passwordhash = generate_password_hash(new_password)
|
||||
user.mustchangepassword = False
|
||||
user.failedlogins = 0
|
||||
user.lockeduntil = None
|
||||
db.session.commit()
|
||||
|
||||
return success_response(
|
||||
{'mustchangepassword': False}, message='Password changed')
|
||||
|
||||
|
||||
@auth_bp.route('/logout', methods=['POST'])
|
||||
@jwt_required()
|
||||
def logout():
|
||||
|
||||
Reference in New Issue
Block a user