Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
af6deec
Perf: set timeout of some steps in KG. (#8867)
KevinHuSh Jul 16, 2025
0359208
Feat: add Kimi model series support (#8866)
yongtenglei Jul 16, 2025
a9c89f2
Fix: fix typo in OpenAI error logging message (#8865)
asiroliu Jul 16, 2025
3f92076
fix: update service_conf.yaml.template (#8863)
griffith-h Jul 16, 2025
32ce515
Merge remote-tracking branch 'origin/main'
Aug 5, 2025
65d2395
Merge remote-tracking branch 'origin/main'
Aug 13, 2025
5b404a9
Merge remote-tracking branch 'origin/main'
Sep 18, 2025
6695d75
Merge remote-tracking branch 'origin/main'
Sep 18, 2025
c918992
Merge remote-tracking branch 'origin/main'
Sep 19, 2025
bcc02fc
Merge remote-tracking branch 'origin/main'
Sep 22, 2025
6ea6ca5
Merge remote-tracking branch 'origin/main'
Nov 5, 2025
5f768a6
Merge remote-tracking branch 'origin/main'
Dec 9, 2025
532193c
Merge remote-tracking branch 'origin/main'
Dec 25, 2025
1d968eb
feat(auth): 支持多点登录会话管理与会话控制接口
Dec 25, 2025
436ce3a
refactor(auth): support multi-session login with user session management
Dec 25, 2025
06ab1fc
refactor(user): optimize user session management API comments and log…
Dec 25, 2025
68823b0
Merge branch 'main' into main
RobertWangWang Dec 25, 2025
202b331
Merge branch 'main' into main
RobertWangWang Dec 25, 2025
7772fe3
convert chinese comments into english comments
Dec 25, 2025
252f386
Merge remote-tracking branch 'origin/main'
Dec 25, 2025
23bb199
Merge branch 'main' into main
RobertWangWang Dec 25, 2025
52a116a
Merge branch 'infiniflow:main' into main
RobertWangWang Dec 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions api/apps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from common.constants import StatusEnum
from api.db.db_models import close_connection, APIToken
from api.db.services import UserService
from api.db.services.user_session_service import UserSessionService
from api.utils.json_encode import CustomJSONEncoder
from api.utils import commands

Expand Down Expand Up @@ -122,6 +123,23 @@ def _load_user():
logging.warning(f"Authentication attempt with invalid token format: {len(access_token)} chars")
return None

# Try to get user from UserSession first (supports multiple login sessions)
try:
session_obj = UserSessionService.get_session_by_token(access_token)
if session_obj:
# Update last activity time
UserSessionService.update_last_activity(access_token)
user = UserService.query(
id=session_obj.get("user_id"), status=StatusEnum.VALID.value
)
if user:
g.user = user[0]
return user[0]
except Exception as session_err:
# UserSession table may not exist yet, or query failed, continue with old method
pass # Silent failure to avoid log clutter

# Fallback to old method (check user.access_token directly)
user = UserService.query(
access_token=access_token, status=StatusEnum.VALID.value
)
Expand All @@ -137,6 +155,7 @@ def _load_user():
return user[0]
except Exception as e:
logging.warning(f"load_user got exception {e}")
return None


current_user = LocalProxy(_load_user)
Expand Down
152 changes: 147 additions & 5 deletions api/apps/user_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from quart import make_response, redirect, request, session
from werkzeug.security import check_password_hash, generate_password_hash
from itsdangerous.url_safe import URLSafeTimedSerializer as Serializer

from api.apps.auth import get_auth_client
from api.db import FileType, UserTenantRole
Expand All @@ -33,6 +34,7 @@
from api.db.services.llm_service import get_init_tenant_llm
from api.db.services.tenant_llm_service import TenantLLMService
from api.db.services.user_service import TenantService, UserService, UserTenantService
from api.db.services.user_session_service import UserSessionService
from common.time_utils import current_timestamp, datetime_format, get_format_time
from common.misc_utils import download_img, get_uuid
from common.constants import RetCode
Expand Down Expand Up @@ -125,14 +127,42 @@ async def login():
)
elif user:
response_data = user.to_json()
user.access_token = get_uuid()
login_user(user)
user.update_time = current_timestamp()
user.update_date = datetime_format(datetime.now())
user.save()

# Create session record (supports multiple login sessions)
auth_token = None
try:
device_name = request.headers.get("User-Agent", "Unknown Device")
ip_address = request.headers.get("X-Forwarded-For") or request.remote_addr
success, session_data = UserSessionService.create_session(
user_id=user.id,
device_name=device_name[:255] if device_name else "Unknown Device",
ip_address=ip_address[:45] if ip_address else "Unknown IP"
)

if success:
# Use UserSession's token
session_token = session_data.get("access_token")
# Update user.access_token to the same value for consistency
user.access_token = session_token
user.save()
# Update access_token in response data
response_data["access_token"] = session_token
auth_token = user.get_id() # Return JWT encoded token
except Exception as e:
# UserSession table may not exist, use old method
logging.debug(f"UserSession creation failed, using old token method: {e}")

# If UserSession creation failed, use old method
if not auth_token:
user.access_token = get_uuid()
user.save()
auth_token = user.get_id()

msg = "Welcome back!"

return await construct_response(data=response_data, auth=user.get_id(), message=msg)
return await construct_response(data=response_data, auth=auth_token, message=msg)
else:
return get_json_result(
data=False,
Expand Down Expand Up @@ -487,7 +517,7 @@ async def user_info_from_github(access_token):
return user_info


@manager.route("/logout", methods=["GET"]) # noqa: F821
@manager.route("/logout", methods=["POST"]) # noqa: F821
@login_required
async def log_out():
"""
Expand All @@ -497,18 +527,130 @@ async def log_out():
- User
security:
- ApiKeyAuth: []
parameters:
- in: body
name: body
required: false
schema:
type: object
properties:
logout_all:
type: boolean
description: Whether to logout all sessions, default false
responses:
200:
description: Logout successful.
schema:
type: object
"""
# Get current token
jwt = Serializer(secret_key=settings.SECRET_KEY)
authorization = request.headers.get("Authorization")
access_token = str(jwt.loads(authorization)) if authorization else None

# Check if logout all sessions is requested
logout_all = False
if request.content_length:
request_data = await get_request_json()
logout_all = request_data.get("logout_all", False)

if logout_all:
# Logout all user sessions
count = UserSessionService.logout_all_sessions(current_user.id)
logging.info(f"User {current_user.email} logged out from {count} sessions")
else:
# Logout only current session
if access_token:
UserSessionService.logout_session(access_token)

# Invalidate old access_token
current_user.access_token = f"INVALID_{secrets.token_hex(16)}"
current_user.save()
logout_user()
return get_json_result(data=True)


@manager.route("/sessions", methods=["GET"]) # noqa: F821
@login_required
async def get_user_sessions():
"""
Get all active sessions for the user
---
tags:
- User
security:
- ApiKeyAuth: []
responses:
200:
description: Session list retrieved successfully
schema:
type: object
properties:
data:
type: array
items:
type: object
"""
sessions = UserSessionService.get_user_sessions(current_user.id)
return get_json_result(data=sessions)


@manager.route("/sessions/<session_id>", methods=["DELETE"]) # noqa: F821
@login_required
async def delete_session(session_id):
"""
Delete a specific session
---
tags:
- User
security:
- ApiKeyAuth: []
parameters:
- in: path
name: session_id
required: true
type: string
description: Session ID
responses:
200:
description: Session deleted successfully
schema:
type: object
"""
# Get session info and validate ownership
from api.db.db_models import UserSession, DB
try:
session_obj = UserSession.select().where(
(UserSession.id == session_id) &
(UserSession.user_id == current_user.id)
).first()

if not session_obj:
return get_json_result(
data=False,
code=RetCode.OPERATING_ERROR,
message="Session not found or access denied"
)

# Logout this session
success = UserSessionService.logout_session(session_obj.access_token)
if success:
return get_json_result(data=True)
else:
return get_json_result(
data=False,
code=RetCode.OPERATING_ERROR,
message="Failed to delete session"
)
except Exception as e:
logging.exception(e)
return get_json_result(
data=False,
code=RetCode.EXCEPTION_ERROR,
message=str(e)
)


@manager.route("/setting", methods=["POST"]) # noqa: F821
@login_required
async def setting_user():
Expand Down
15 changes: 15 additions & 0 deletions api/db/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,21 @@ def fill_db_model_object(model_object, human_model_dict):
return model_object


class UserSession(DataBaseModel):
"""User session table, supports multiple login sessions"""
id = CharField(max_length=32, primary_key=True)
user_id = CharField(max_length=32, null=False, index=True)
access_token = CharField(max_length=255, null=False, index=True)
device_name = CharField(max_length=255, null=True, help_text="Device name or browser info")
ip_address = CharField(max_length=45, null=True, help_text="IP address")
is_active = CharField(max_length=1, null=False, default="1", index=True)
last_activity_time = BigIntegerField(null=True, index=True)
expires_at = BigIntegerField(null=True, index=True, help_text="Session expiration time")

class Meta:
db_table = "user_session"


class User(DataBaseModel, AuthUser):
id = CharField(max_length=32, primary_key=True)
access_token = CharField(max_length=255, null=True, index=True)
Expand Down
Loading