mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-31 14:47:13 +00:00
Merge remote-tracking branch 'origin/master' into dev/4.11.x
# Conflicts: # src/langbot/pkg/pipeline/preproc/preproc.py # src/langbot/pkg/pipeline/process/handlers/chat.py # src/langbot/pkg/provider/runners/localagent.py # src/langbot/pkg/provider/tools/toolmgr.py # src/langbot/templates/metadata/pipeline/ai.yaml # tests/unit_tests/test_preproc.py # web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx # web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx
This commit is contained in:
@@ -86,6 +86,10 @@ class PipelinesRouterGroup(group.RouterGroup):
|
||||
'available_plugins': plugins,
|
||||
'bound_mcp_servers': extensions_prefs.get('mcp_servers', []),
|
||||
'available_mcp_servers': mcp_servers,
|
||||
'bound_mcp_resources': extensions_prefs.get('mcp_resources', []),
|
||||
'mcp_resource_agent_read_enabled': extensions_prefs.get(
|
||||
'mcp_resource_agent_read_enabled', True
|
||||
),
|
||||
'bound_skills': extensions_prefs.get('skills', []),
|
||||
'available_skills': available_skills,
|
||||
}
|
||||
@@ -99,6 +103,8 @@ class PipelinesRouterGroup(group.RouterGroup):
|
||||
bound_plugins = json_data.get('bound_plugins', [])
|
||||
bound_mcp_servers = json_data.get('bound_mcp_servers', [])
|
||||
bound_skills = json_data.get('bound_skills', [])
|
||||
bound_mcp_resources = json_data.get('bound_mcp_resources')
|
||||
mcp_resource_agent_read_enabled = json_data.get('mcp_resource_agent_read_enabled')
|
||||
|
||||
await self.ap.pipeline_service.update_pipeline_extensions(
|
||||
pipeline_uuid,
|
||||
@@ -108,6 +114,8 @@ class PipelinesRouterGroup(group.RouterGroup):
|
||||
enable_all_mcp_servers,
|
||||
bound_skills=bound_skills,
|
||||
enable_all_skills=enable_all_skills,
|
||||
bound_mcp_resources=bound_mcp_resources,
|
||||
mcp_resource_agent_read_enabled=mcp_resource_agent_read_enabled,
|
||||
)
|
||||
|
||||
return self.success()
|
||||
|
||||
@@ -18,7 +18,6 @@ class BotsRouterGroup(group.RouterGroup):
|
||||
@self.route('/<bot_uuid>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||
async def _(bot_uuid: str) -> str:
|
||||
if quart.request.method == 'GET':
|
||||
# 返回运行时信息,包括webhook地址等
|
||||
bot = await self.ap.bot_service.get_runtime_bot_info(bot_uuid)
|
||||
if bot is None:
|
||||
return self.http_status(404, -1, 'bot not found')
|
||||
@@ -37,30 +36,21 @@ class BotsRouterGroup(group.RouterGroup):
|
||||
from_index = json_data.get('from_index', -1)
|
||||
max_count = json_data.get('max_count', 10)
|
||||
logs, total_count = await self.ap.bot_service.list_event_logs(bot_uuid, from_index, max_count)
|
||||
return self.success(
|
||||
data={
|
||||
'logs': logs,
|
||||
'total_count': total_count,
|
||||
}
|
||||
)
|
||||
return self.success(data={'logs': logs, 'total_count': total_count})
|
||||
|
||||
@self.route('/<bot_uuid>/send_message', methods=['POST'], auth_type=group.AuthType.API_KEY)
|
||||
async def _(bot_uuid: str) -> str:
|
||||
"""Send message to a specific target via bot"""
|
||||
json_data = await quart.request.json
|
||||
target_type = json_data.get('target_type')
|
||||
target_id = json_data.get('target_id')
|
||||
message_chain_data = json_data.get('message_chain')
|
||||
|
||||
# Validate required fields
|
||||
if not target_type:
|
||||
return self.http_status(400, -1, 'target_type is required')
|
||||
if not target_id:
|
||||
return self.http_status(400, -1, 'target_id is required')
|
||||
if not message_chain_data:
|
||||
return self.http_status(400, -1, 'message_chain is required')
|
||||
|
||||
# Validate target_type
|
||||
if target_type not in ['person', 'group']:
|
||||
return self.http_status(400, -1, 'target_type must be either "person" or "group"')
|
||||
|
||||
@@ -72,3 +62,29 @@ class BotsRouterGroup(group.RouterGroup):
|
||||
|
||||
traceback.print_exc()
|
||||
return self.http_status(500, -1, f'Failed to send message: {str(e)}')
|
||||
|
||||
# ============ Bot Admins ============
|
||||
|
||||
@self.route('/<bot_uuid>/admins', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
||||
async def _(bot_uuid: str) -> str:
|
||||
if quart.request.method == 'GET':
|
||||
admins = await self.ap.bot_service.get_bot_admins(bot_uuid)
|
||||
return self.success(data={'admins': admins})
|
||||
elif quart.request.method == 'POST':
|
||||
json_data = await quart.request.json
|
||||
launcher_type = json_data.get('launcher_type', '').strip()
|
||||
launcher_id = str(json_data.get('launcher_id', '')).strip()
|
||||
if not launcher_type or not launcher_id:
|
||||
return self.http_status(400, -1, 'launcher_type and launcher_id are required')
|
||||
try:
|
||||
admin_id = await self.ap.bot_service.add_bot_admin(bot_uuid, launcher_type, launcher_id)
|
||||
return self.success(data={'id': admin_id})
|
||||
except Exception as e:
|
||||
return self.http_status(409, -1, str(e))
|
||||
|
||||
@self.route(
|
||||
'/<bot_uuid>/admins/<int:admin_id>', methods=['DELETE'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
|
||||
)
|
||||
async def _(bot_uuid: str, admin_id: int) -> str:
|
||||
await self.ap.bot_service.delete_bot_admin(bot_uuid, admin_id)
|
||||
return self.success()
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import quart
|
||||
import traceback
|
||||
from urllib.parse import unquote
|
||||
|
||||
|
||||
from ... import group
|
||||
@@ -66,3 +67,50 @@ class MCPRouterGroup(group.RouterGroup):
|
||||
server_data = await quart.request.json
|
||||
task_id = await self.ap.mcp_service.test_mcp_server(server_name=server_name, server_data=server_data)
|
||||
return self.success(data={'task_id': task_id})
|
||||
|
||||
@self.route('/servers/<server_name>/resources', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _(server_name: str) -> str:
|
||||
"""Get resources from an MCP server"""
|
||||
server_name = unquote(server_name)
|
||||
try:
|
||||
resources = await self.ap.mcp_service.get_mcp_server_resources(server_name)
|
||||
templates = await self.ap.mcp_service.get_mcp_server_resource_templates(server_name)
|
||||
runtime_info = await self.ap.mcp_service.get_runtime_info(server_name)
|
||||
return self.success(
|
||||
data={
|
||||
'resources': resources,
|
||||
'resource_templates': templates,
|
||||
'resource_capabilities': (runtime_info or {}).get('resource_capabilities', {}),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
return self.http_status(500, -1, f'Failed to get resources: {str(e)}')
|
||||
|
||||
@self.route('/servers/<server_name>/resource-templates', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _(server_name: str) -> str:
|
||||
"""Get resource templates from an MCP server"""
|
||||
server_name = unquote(server_name)
|
||||
try:
|
||||
templates = await self.ap.mcp_service.get_mcp_server_resource_templates(server_name)
|
||||
return self.success(data={'resource_templates': templates})
|
||||
except Exception as e:
|
||||
return self.http_status(500, -1, f'Failed to get resource templates: {str(e)}')
|
||||
|
||||
@self.route('/servers/<server_name>/resources/read', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _(server_name: str) -> str:
|
||||
"""Read a resource from an MCP server"""
|
||||
server_name = unquote(server_name)
|
||||
data = await quart.request.json
|
||||
uri = data.get('uri')
|
||||
if not uri:
|
||||
return self.http_status(400, -1, 'URI is required')
|
||||
try:
|
||||
envelope = await self.ap.mcp_service.read_mcp_server_resource_envelope(
|
||||
server_name,
|
||||
uri,
|
||||
max_bytes=data.get('max_bytes'),
|
||||
include_blob=bool(data.get('include_blob', False)),
|
||||
)
|
||||
return self.success(data=envelope)
|
||||
except Exception as e:
|
||||
return self.http_status(500, -1, f'Failed to read resource: {str(e)}')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import quart
|
||||
|
||||
from ... import group
|
||||
|
||||
|
||||
@@ -9,25 +11,41 @@ class ToolsRouterGroup(group.RouterGroup):
|
||||
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _() -> str:
|
||||
"""获取所有可用工具列表"""
|
||||
tools = await self.ap.tool_mgr.get_all_tools()
|
||||
pipeline_uuid = quart.request.args.get('pipeline_uuid') or quart.request.args.get('pipeline_id')
|
||||
bound_plugins: list[str] | None = None
|
||||
bound_mcp_servers: list[str] | None = None
|
||||
|
||||
tool_list = []
|
||||
for tool in tools:
|
||||
tool_list.append(
|
||||
{
|
||||
'name': tool.name,
|
||||
'description': tool.description,
|
||||
'human_desc': tool.human_desc,
|
||||
'parameters': tool.parameters,
|
||||
}
|
||||
)
|
||||
if pipeline_uuid:
|
||||
pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_uuid)
|
||||
if pipeline is None:
|
||||
return self.http_status(404, -1, 'pipeline not found')
|
||||
|
||||
return self.success(data={'tools': tool_list})
|
||||
extensions_prefs = pipeline.get('extensions_preferences', {}) or {}
|
||||
if not extensions_prefs.get('enable_all_plugins', True):
|
||||
bound_plugins = [
|
||||
f'{plugin.get("author", "")}/{plugin.get("name", "")}'
|
||||
for plugin in extensions_prefs.get('plugins', [])
|
||||
if isinstance(plugin, dict) and plugin.get('name')
|
||||
]
|
||||
if not extensions_prefs.get('enable_all_mcp_servers', True):
|
||||
bound_mcp_servers = [
|
||||
server for server in (extensions_prefs.get('mcp_servers', []) or []) if isinstance(server, str)
|
||||
]
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'tools': await self.ap.tool_mgr.get_tool_catalog(
|
||||
bound_plugins,
|
||||
bound_mcp_servers,
|
||||
include_skill_authoring=True,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/<tool_name>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _(tool_name: str) -> str:
|
||||
"""获取特定工具详情"""
|
||||
tools = await self.ap.tool_mgr.get_all_tools()
|
||||
tools = await self.ap.tool_mgr.get_all_tools(include_skill_authoring=True)
|
||||
|
||||
for tool in tools:
|
||||
if tool.name == tool_name:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import base64
|
||||
|
||||
import quart
|
||||
|
||||
from .. import group
|
||||
@@ -30,6 +32,50 @@ class SurveyRouterGroup(group.RouterGroup):
|
||||
return self.fail(2, 'Failed to submit response')
|
||||
return self.fail(3, 'Survey not available')
|
||||
|
||||
@self.route('/feedback', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _feedback(user_email: str) -> str:
|
||||
"""Submit on-demand user feedback from the sidebar."""
|
||||
json_data = await quart.request.get_json(silent=True) or {}
|
||||
content = str(json_data.get('content', '')).strip()
|
||||
attachments = json_data.get('attachments', [])
|
||||
|
||||
if not content:
|
||||
return self.fail(1, 'content required')
|
||||
if len(content) > 5000:
|
||||
return self.fail(2, 'content too long')
|
||||
if not isinstance(attachments, list):
|
||||
return self.fail(3, 'attachments must be an array')
|
||||
if len(attachments) > 3:
|
||||
return self.fail(4, 'too many attachments')
|
||||
|
||||
normalized_attachments = []
|
||||
for item in attachments:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
data_url = str(item.get('data_url', ''))
|
||||
mime_type = str(item.get('mime_type', ''))[:128]
|
||||
name = str(item.get('name', ''))[:255]
|
||||
if not data_url.startswith('data:image/'):
|
||||
continue
|
||||
try:
|
||||
payload = data_url.split(',', 1)[1]
|
||||
if len(base64.b64decode(payload, validate=True)) > 1024 * 1024:
|
||||
return self.fail(5, 'attachment too large')
|
||||
except Exception:
|
||||
return self.fail(5, 'attachment too large')
|
||||
normalized_attachments.append({'name': name, 'mime_type': mime_type, 'data_url': data_url})
|
||||
|
||||
if self.ap.survey:
|
||||
ok = await self.ap.survey.submit_feedback(
|
||||
content=content,
|
||||
attachments=normalized_attachments,
|
||||
user_email=user_email,
|
||||
)
|
||||
if ok:
|
||||
return self.success()
|
||||
return self.fail(6, 'Failed to submit feedback')
|
||||
return self.fail(7, 'Survey not available')
|
||||
|
||||
@self.route('/dismiss', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _dismiss() -> str:
|
||||
"""Dismiss survey."""
|
||||
|
||||
@@ -195,6 +195,13 @@ class UserRouterGroup(group.RouterGroup):
|
||||
@self.route('/set-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _(user_email: str) -> str:
|
||||
"""Set password for Space account (first time) or change password"""
|
||||
# Check if modifying login info is allowed
|
||||
allow_modify_login_info = self.ap.instance_config.data.get('system', {}).get(
|
||||
'allow_modify_login_info', True
|
||||
)
|
||||
if not allow_modify_login_info:
|
||||
return self.http_status(403, -1, 'Modifying login info is disabled')
|
||||
|
||||
json_data = await quart.request.json
|
||||
new_password = json_data.get('new_password')
|
||||
current_password = json_data.get('current_password')
|
||||
|
||||
@@ -274,3 +274,35 @@ class BotService:
|
||||
|
||||
# Send message via adapter
|
||||
await runtime_bot.adapter.send_message(target_type, str(target_id), message_chain)
|
||||
|
||||
# ============ Bot Admins ============
|
||||
|
||||
async def get_bot_admins(self, bot_uuid: str) -> list[dict]:
|
||||
from ....entity.persistence import bot as persistence_bot
|
||||
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_bot.BotAdmin).where(persistence_bot.BotAdmin.bot_uuid == bot_uuid)
|
||||
)
|
||||
return [{'id': r.id, 'launcher_type': r.launcher_type, 'launcher_id': r.launcher_id} for r in result.all()]
|
||||
|
||||
async def add_bot_admin(self, bot_uuid: str, launcher_type: str, launcher_id: str) -> int:
|
||||
from ....entity.persistence import bot as persistence_bot
|
||||
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.insert(persistence_bot.BotAdmin).values(
|
||||
bot_uuid=bot_uuid,
|
||||
launcher_type=launcher_type,
|
||||
launcher_id=launcher_id,
|
||||
)
|
||||
)
|
||||
return result.inserted_primary_key[0]
|
||||
|
||||
async def delete_bot_admin(self, bot_uuid: str, admin_id: int) -> None:
|
||||
from ....entity.persistence import bot as persistence_bot
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.delete(persistence_bot.BotAdmin).where(
|
||||
persistence_bot.BotAdmin.bot_uuid == bot_uuid,
|
||||
persistence_bot.BotAdmin.id == admin_id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -136,6 +136,32 @@ class MCPService:
|
||||
if server_name in self.ap.tool_mgr.mcp_tool_loader.sessions:
|
||||
await self.ap.tool_mgr.mcp_tool_loader.remove_mcp_server(server_name)
|
||||
|
||||
async def get_mcp_server_resources(self, server_name: str) -> list[dict]:
|
||||
"""Get resources from a specific MCP server."""
|
||||
return await self.ap.tool_mgr.mcp_tool_loader.get_resources(server_name)
|
||||
|
||||
async def get_mcp_server_resource_templates(self, server_name: str) -> list[dict]:
|
||||
"""Get resource templates from a specific MCP server."""
|
||||
return await self.ap.tool_mgr.mcp_tool_loader.get_resource_templates(server_name)
|
||||
|
||||
async def read_mcp_server_resource_envelope(
|
||||
self,
|
||||
server_name: str,
|
||||
uri: str,
|
||||
*,
|
||||
max_bytes: int | None = None,
|
||||
include_blob: bool = False,
|
||||
) -> dict:
|
||||
"""Read a resource from a specific MCP server with metadata."""
|
||||
kwargs = {'include_blob': include_blob, 'source': 'ui_preview'}
|
||||
if max_bytes is not None:
|
||||
kwargs['max_bytes'] = max_bytes
|
||||
return await self.ap.tool_mgr.mcp_tool_loader.read_resource_envelope(server_name, uri, **kwargs)
|
||||
|
||||
async def read_mcp_server_resource(self, server_name: str, uri: str) -> list[dict]:
|
||||
"""Read a resource from a specific MCP server."""
|
||||
return await self.ap.tool_mgr.mcp_tool_loader.read_resource(server_name, uri)
|
||||
|
||||
async def test_mcp_server(self, server_name: str, server_data: dict) -> int:
|
||||
"""测试 MCP 服务器连接并返回任务 ID"""
|
||||
|
||||
|
||||
@@ -185,6 +185,8 @@ class PipelineService:
|
||||
'enable_all_mcp_servers': True,
|
||||
'plugins': [],
|
||||
'mcp_servers': [],
|
||||
'mcp_resources': [],
|
||||
'mcp_resource_agent_read_enabled': True,
|
||||
}
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
@@ -284,6 +286,8 @@ class PipelineService:
|
||||
'enable_all_mcp_servers': True,
|
||||
'plugins': [],
|
||||
'mcp_servers': [],
|
||||
'mcp_resources': [],
|
||||
'mcp_resource_agent_read_enabled': True,
|
||||
}
|
||||
),
|
||||
}
|
||||
@@ -308,6 +312,8 @@ class PipelineService:
|
||||
enable_all_mcp_servers: bool = True,
|
||||
bound_skills: list[str] = None,
|
||||
enable_all_skills: bool = True,
|
||||
bound_mcp_resources: list[dict] = None,
|
||||
mcp_resource_agent_read_enabled: bool | None = None,
|
||||
) -> None:
|
||||
"""Update the bound plugins and MCP servers for a pipeline"""
|
||||
# Get current pipeline
|
||||
@@ -327,10 +333,14 @@ class PipelineService:
|
||||
extensions_preferences['enable_all_mcp_servers'] = enable_all_mcp_servers
|
||||
extensions_preferences['enable_all_skills'] = enable_all_skills
|
||||
extensions_preferences['plugins'] = bound_plugins
|
||||
if mcp_resource_agent_read_enabled is not None:
|
||||
extensions_preferences['mcp_resource_agent_read_enabled'] = mcp_resource_agent_read_enabled
|
||||
if bound_mcp_servers is not None:
|
||||
extensions_preferences['mcp_servers'] = bound_mcp_servers
|
||||
if bound_skills is not None:
|
||||
extensions_preferences['skills'] = bound_skills
|
||||
if bound_mcp_resources is not None:
|
||||
extensions_preferences['mcp_resources'] = bound_mcp_resources
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||
|
||||
@@ -20,6 +20,15 @@ class UserService:
|
||||
def __init__(self, ap: app.Application) -> None:
|
||||
self.ap = ap
|
||||
self._create_user_lock = asyncio.Lock()
|
||||
self._password_hash_lock = asyncio.Semaphore(1)
|
||||
|
||||
async def _hash_password(self, password: str) -> str:
|
||||
async with self._password_hash_lock:
|
||||
return await asyncio.to_thread(argon2.PasswordHasher().hash, password)
|
||||
|
||||
async def _verify_password(self, hashed_password: str, password: str) -> None:
|
||||
async with self._password_hash_lock:
|
||||
await asyncio.to_thread(argon2.PasswordHasher().verify, hashed_password, password)
|
||||
|
||||
async def is_initialized(self) -> bool:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(user.User).limit(1))
|
||||
@@ -28,9 +37,7 @@ class UserService:
|
||||
return result_list is not None and len(result_list) > 0
|
||||
|
||||
async def create_user(self, user_email: str, password: str) -> None:
|
||||
ph = argon2.PasswordHasher()
|
||||
|
||||
hashed_password = ph.hash(password)
|
||||
hashed_password = await self._hash_password(password)
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.insert(user.User).values(user=user_email, password=hashed_password, account_type='local')
|
||||
@@ -69,9 +76,7 @@ class UserService:
|
||||
if not user_obj.password:
|
||||
raise ValueError('请使用 Space 账户登录')
|
||||
|
||||
ph = argon2.PasswordHasher()
|
||||
|
||||
ph.verify(user_obj.password, password)
|
||||
await self._verify_password(user_obj.password, password)
|
||||
|
||||
return await self.generate_jwt_token(user_email)
|
||||
|
||||
@@ -93,17 +98,13 @@ class UserService:
|
||||
return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user']
|
||||
|
||||
async def reset_password(self, user_email: str, new_password: str) -> None:
|
||||
ph = argon2.PasswordHasher()
|
||||
|
||||
hashed_password = ph.hash(new_password)
|
||||
hashed_password = await self._hash_password(new_password)
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
||||
)
|
||||
|
||||
async def change_password(self, user_email: str, current_password: str, new_password: str) -> None:
|
||||
ph = argon2.PasswordHasher()
|
||||
|
||||
user_obj = await self.get_user_by_email(user_email)
|
||||
if user_obj is None:
|
||||
raise ValueError('User not found')
|
||||
@@ -111,9 +112,9 @@ class UserService:
|
||||
if not user_obj.password:
|
||||
raise ValueError('No local password set, please set a password first')
|
||||
|
||||
ph.verify(user_obj.password, current_password)
|
||||
await self._verify_password(user_obj.password, current_password)
|
||||
|
||||
hashed_password = ph.hash(new_password)
|
||||
hashed_password = await self._hash_password(new_password)
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
||||
@@ -232,7 +233,6 @@ class UserService:
|
||||
|
||||
async def set_password(self, user_email: str, new_password: str, current_password: str | None = None) -> None:
|
||||
"""Set or change password for a user"""
|
||||
ph = argon2.PasswordHasher()
|
||||
user_obj = await self.get_user_by_email(user_email)
|
||||
|
||||
if user_obj is None:
|
||||
@@ -243,9 +243,9 @@ class UserService:
|
||||
if has_password:
|
||||
if not current_password:
|
||||
raise ValueError('Current password is required')
|
||||
ph.verify(user_obj.password, current_password)
|
||||
await self._verify_password(user_obj.password, current_password)
|
||||
|
||||
hashed_password = ph.hash(new_password)
|
||||
hashed_password = await self._hash_password(new_password)
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user