Skip to content

Commit 4e4c6fb

Browse files
authored
add login logs (#76)
* simplify crud method naming * update get_user_list to get_select * add sign in logs * Perform pre-commit fix * Encapsulated request ip address resolution * Delete login log records for uncertain exceptions * Add login log deletion interface * Add login logging to background tasks * update the user agent parse
1 parent 9b5a19a commit 4e4c6fb

File tree

17 files changed

+318
-53
lines changed

17 files changed

+318
-53
lines changed

backend/app/api/routers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
from backend.app.api.v1.role import router as role_router
1010
from backend.app.api.v1.menu import router as menu_router
1111
from backend.app.api.v1.api import router as api_router
12-
from backend.app.api.v1.task_demo import router as task_demo_router
1312
from backend.app.api.v1.config import router as config_router
13+
from backend.app.api.v1.login_log import router as login_log_router
14+
from backend.app.api.v1.task_demo import router as task_demo_router
1415

1516
v1 = APIRouter(prefix='/v1')
1617

@@ -22,4 +23,5 @@
2223
v1.include_router(menu_router, prefix='/menus', tags=['菜单管理'])
2324
v1.include_router(api_router, prefix='/apis', tags=['API管理'])
2425
v1.include_router(config_router, prefix='/configs', tags=['系统配置'])
26+
v1.include_router(login_log_router, prefix='/login_logs', tags=['登录日志管理'])
2527
v1.include_router(task_demo_router, prefix='/tasks', tags=['任务管理'])

backend/app/api/v1/auth/auth.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fastapi import APIRouter, Depends, Request
44
from fastapi.security import OAuth2PasswordRequestForm
55
from fastapi_limiter.depends import RateLimiter
6+
from starlette.background import BackgroundTasks
67

78
from backend.app.common.jwt import DependsUser, get_token, jwt_decode, CurrentJwtAuth
89
from backend.app.common.response.response_schema import response_base
@@ -15,7 +16,7 @@
1516

1617
@router.post('/swagger_login', summary='swagger 表单登录', description='form 格式登录,仅用于 swagger 文档调试接口')
1718
async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -> SwaggerToken:
18-
token, user = await UserService.swagger_login(form_data)
19+
token, user = await UserService().swagger_login(form_data)
1920
return SwaggerToken(access_token=token, user=user)
2021

2122

@@ -25,8 +26,10 @@ async def swagger_user_login(form_data: OAuth2PasswordRequestForm = Depends()) -
2526
description='json 格式登录, 仅支持在第三方api工具调试接口, 例如: postman',
2627
dependencies=[Depends(RateLimiter(times=5, minutes=15))],
2728
)
28-
async def user_login(obj: Auth):
29-
access_token, refresh_token, access_expire, refresh_expire, user = await UserService.login(obj)
29+
async def user_login(request: Request, obj: Auth, background_tasks: BackgroundTasks):
30+
access_token, refresh_token, access_expire, refresh_expire, user = await UserService().login(
31+
request=request, obj=obj, background_tasks=background_tasks
32+
)
3033
data = LoginToken(
3134
access_token=access_token,
3235
refresh_token=refresh_token,
@@ -41,7 +44,7 @@ async def user_login(obj: Auth):
4144
async def get_refresh_token(request: Request, custom_time: RefreshTokenTime):
4245
token = get_token(request)
4346
user_id, _ = jwt_decode(token)
44-
refresh_token, refresh_expire = await UserService.refresh_token(user_id, custom_time)
47+
refresh_token, refresh_expire = await UserService.refresh_token(user_id=user_id, custom_time=custom_time)
4548
data = RefreshToken(refresh_token=refresh_token, refresh_token_expire_time=refresh_expire)
4649
return response_base.success(data=data)
4750

backend/app/api/v1/login_log.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from typing import Annotated
4+
5+
from fastapi import APIRouter, Query
6+
7+
from backend.app.common.casbin_rbac import DependsRBAC
8+
from backend.app.common.jwt import DependsUser
9+
from backend.app.common.pagination import paging_data, PageDepends
10+
from backend.app.common.response.response_schema import response_base
11+
from backend.app.database.db_mysql import CurrentSession
12+
from backend.app.schemas.login_log import GetAllLoginLog
13+
from backend.app.services.login_log_service import LoginLogService
14+
15+
router = APIRouter()
16+
17+
18+
@router.get('', summary='获取所有登录日志', dependencies=[DependsUser, PageDepends])
19+
async def get_all_login_logs(db: CurrentSession):
20+
log_select = await LoginLogService.get_select()
21+
page_data = await paging_data(db, log_select, GetAllLoginLog)
22+
return response_base.success(data=page_data)
23+
24+
25+
@router.delete('', summary='(批量)删除登录日志', dependencies=[DependsRBAC])
26+
async def delete_login_log(pk: Annotated[list[int], Query(...)]):
27+
count = await LoginLogService.delete(pk)
28+
if count > 0:
29+
return response_base.success()
30+
return response_base.fail()
31+
32+
33+
@router.delete('/all', summary='清空登录日志', dependencies=[DependsRBAC])
34+
async def delete_all_login_logs():
35+
count = await LoginLogService.delete_all()
36+
if count > 0:
37+
return response_base.success()
38+
return response_base.fail()

backend/app/api/v1/user.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from backend.app.common.pagination import paging_data, PageDepends
77
from backend.app.common.response.response_schema import response_base
88
from backend.app.database.db_mysql import CurrentSession
9-
from backend.app.schemas.user import CreateUser, GetUserInfo, ResetPassword, UpdateUser, Avatar
9+
from backend.app.schemas.user import CreateUser, GetAllUserInfo, ResetPassword, UpdateUser, Avatar
1010
from backend.app.services.user_service import UserService
1111
from backend.app.utils.serializers import select_to_json
1212

@@ -21,14 +21,16 @@ async def user_register(obj: CreateUser):
2121

2222
@router.post('/password/reset', summary='密码重置')
2323
async def password_reset(obj: ResetPassword):
24-
await UserService.pwd_reset(obj)
25-
return response_base.success()
24+
count = await UserService.pwd_reset(obj)
25+
if count > 0:
26+
return response_base.success()
27+
return response_base.fail()
2628

2729

2830
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsUser])
2931
async def userinfo(username: str):
3032
current_user = await UserService.get_userinfo(username)
31-
data = GetUserInfo(**select_to_json(current_user))
33+
data = GetAllUserInfo(**select_to_json(current_user))
3234
return response_base.success(data=data)
3335

3436

@@ -51,7 +53,7 @@ async def update_avatar(username: str, avatar: Avatar, current_user: CurrentUser
5153
@router.get('', summary='获取所有用户', dependencies=[DependsUser, PageDepends])
5254
async def get_all_users(db: CurrentSession):
5355
user_select = await UserService.get_select()
54-
page_data = await paging_data(db, user_select, GetUserInfo)
56+
page_data = await paging_data(db, user_select, GetAllUserInfo)
5557
return response_base.success(data=page_data)
5658

5759

backend/app/common/exception/exception_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,6 @@ def all_exception_handler(request: Request, exc: Exception):
109109
return JSONResponse(
110110
status_code=500,
111111
content=response_base.fail(code=500, msg=str(exc))
112-
if settings.UVICORN_RELOAD
112+
if settings.ENVIRONMENT != 'pro'
113113
else response_base.fail(code=500, msg='Internal Server Error'),
114114
)

backend/app/core/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def validator_api_url(cls, values):
5454
# Static Server
5555
STATIC_FILES: bool = False
5656

57+
# Location Parse
58+
LOCATION_PARSE: bool = True # 将会导致登录延时,建议关闭,有条件自行使用第三方离线数据库
59+
5760
# MySQL
5861
DB_ECHO: bool = False
5962
DB_DATABASE: str = 'fba'

backend/app/crud/crud_login_log.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from typing import NoReturn
4+
5+
from sqlalchemy import Select, select, desc, delete
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
from backend.app.crud.base import CRUDBase
9+
from backend.app.models import LoginLog
10+
from backend.app.schemas.login_log import CreateLoginLog, UpdateLoginLog
11+
12+
13+
class CRUDLoginLog(CRUDBase[LoginLog, CreateLoginLog, UpdateLoginLog]):
14+
async def get_all(self) -> Select:
15+
return select(self.model).order_by(desc(self.model.create_time))
16+
17+
async def create(self, db: AsyncSession, obj_in: CreateLoginLog) -> NoReturn:
18+
await self.create_(db, obj_in)
19+
await db.commit()
20+
21+
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
22+
logs = await db.execute(delete(self.model).where(self.model.id.in_(pk)))
23+
return logs.rowcount
24+
25+
async def delete_all(self, db: AsyncSession) -> int:
26+
logs = await db.execute(delete(self.model))
27+
return logs.rowcount
28+
29+
30+
LoginLogDao: CRUDLoginLog = CRUDLoginLog(LoginLog)

backend/app/crud/crud_user.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3+
from datetime import datetime
34
from typing import NoReturn
45

5-
from sqlalchemy import func, select, update, desc
6+
from sqlalchemy import select, update, desc
67
from sqlalchemy.ext.asyncio import AsyncSession
78
from sqlalchemy.orm import selectinload
89
from sqlalchemy.sql import Select
@@ -21,8 +22,8 @@ async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
2122
user = await db.execute(select(self.model).where(self.model.username == username))
2223
return user.scalars().first()
2324

24-
async def update_login_time(self, db: AsyncSession, username: str) -> int:
25-
user = await db.execute(update(self.model).where(self.model.username == username).values(last_login=func.now()))
25+
async def update_login_time(self, db: AsyncSession, username: str, login_time: datetime) -> int:
26+
user = await db.execute(update(self.model).where(self.model.username == username).values(last_login=login_time))
2627
return user.rowcount
2728

2829
async def create(self, db: AsyncSession, create: CreateUser) -> NoReturn:
@@ -53,7 +54,7 @@ async def update_avatar(self, db: AsyncSession, current_user: User, avatar: Avat
5354
return user.rowcount
5455

5556
async def delete(self, db: AsyncSession, user_id: int) -> int:
56-
return await super().delete_(db, user_id)
57+
return await self.delete_(db, user_id)
5758

5859
async def check_email(self, db: AsyncSession, email: str) -> User | None:
5960
mail = await db.execute(select(self.model).where(self.model.email == email))

backend/app/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
from backend.app.models.sys_menu import Menu
1212
from backend.app.models.sys_role import Role
1313
from backend.app.models.sys_user import User
14+
from backend.app.models.sys_login_log import LoginLog

backend/app/models/sys_login_log.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
from datetime import datetime
4+
5+
from sqlalchemy import String, func
6+
from sqlalchemy.orm import Mapped, mapped_column
7+
8+
from backend.app.database.base_class import DataClassBase, id_key
9+
10+
11+
class LoginLog(DataClassBase):
12+
"""登录日志表"""
13+
14+
__tablename__ = 'sys_login_log'
15+
16+
id: Mapped[id_key] = mapped_column(init=False)
17+
user_uuid: Mapped[str] = mapped_column(String(50), nullable=False, comment='用户UUID')
18+
username: Mapped[str] = mapped_column(String(20), nullable=False, comment='用户名')
19+
status: Mapped[int] = mapped_column(insert_default=0, comment='登录状态(0失败 1成功)')
20+
ipaddr: Mapped[str] = mapped_column(String(50), nullable=False, comment='登录IP地址')
21+
location: Mapped[str] = mapped_column(String(255), nullable=False, comment='归属地')
22+
browser: Mapped[str] = mapped_column(String(255), nullable=False, comment='浏览器')
23+
os: Mapped[str] = mapped_column(String(255), nullable=False, comment='操作系统')
24+
msg: Mapped[str] = mapped_column(String(255), nullable=False, comment='提示消息')
25+
login_time: Mapped[datetime] = mapped_column(nullable=False, comment='登录时间')
26+
create_time: Mapped[datetime] = mapped_column(init=False, default=func.now(), comment='创建时间')

0 commit comments

Comments
 (0)