This commit is contained in:
ciomek committed 2026-09-04 17:31:30 +02:00
1 parent 5732e26380
commit dbed39ee3a
11 files changed
+122 -61

No files matched your search

@@ -1,8 +1,8 @@
"""logs """init
Revision ID: 69a46d2e17df Revision ID: 0f809a005dc6
Revises: 2df7e2c6af43 Revises:
Create Date: 2026-09-03 22:23:08.848825 Create Date: 2026-09-04 17:30:49.572225
""" """
from typing import Sequence, Union from typing import Sequence, Union
@@ -13,8 +13,8 @@ import sqlalchemy as sa
import sqlmodel import sqlmodel
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '69a46d2e17df' revision: str = '0f809a005dc6'
down_revision: Union[str, Sequence[str], None] = '2df7e2c6af43' down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
@@ -28,6 +28,13 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('code') sa.UniqueConstraint('code')
) )
op.create_table('minecraftuser',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('username', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid')
)
op.create_table('logentry', op.create_table('logentry',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('log_type_id', sa.Integer(), nullable=False), sa.Column('log_type_id', sa.Integer(), nullable=False),
@@ -36,12 +43,25 @@ def upgrade() -> None:
sa.ForeignKeyConstraint(['log_type_id'], ['logtype.id'], ), sa.ForeignKeyConstraint(['log_type_id'], ['logtype.id'], ),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('minecraft_user_id', sa.Integer(), nullable=False),
sa.Column('internal_username', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('password', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('admin', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['minecraft_user_id'], ['minecraftuser.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('internal_username'),
sa.UniqueConstraint('minecraft_user_id')
)
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
"""Downgrade schema.""" """Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
op.drop_table('logentry') op.drop_table('logentry')
op.drop_table('minecraftuser')
op.drop_table('logtype') op.drop_table('logtype')
# ### end Alembic commands ### # ### end Alembic commands ###
-42
View File
@@ -1,42 +0,0 @@
"""Users
Revision ID: 2df7e2c6af43
Revises:
Create Date: 2026-08-17 12:42:05.103510
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = '2df7e2c6af43'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('internal_username', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('password', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('admin', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('internal_username'),
sa.UniqueConstraint('uuid')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###
@@ -6,7 +6,7 @@ from sqlmodel import Session, select
from loon.web.db import engine from loon.web.db import engine
from loon.web.logs.service import write_log from loon.web.logs.service import write_log
from loon.web.logs.types import LogTypeEnum from loon.web.logs.types import LogTypeEnum
from loon.web.users.models import User from loon.web.users.models import MinecraftUser, User
async def change_password_handler(client, userdata, msg, data): async def change_password_handler(client, userdata, msg, data):
@@ -24,7 +24,7 @@ async def change_password_handler(client, userdata, msg, data):
password = data["password"] password = data["password"]
with Session(engine) as session: with Session(engine) as session:
statement = select(User).where(User.uuid == uuid) statement = select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
user = session.exec(statement).first() user = session.exec(statement).first()
if user is None: if user is None:
@@ -33,7 +33,7 @@ async def change_password_handler(client, userdata, msg, data):
return return
user = session.exec( user = session.exec(
select(User).where(User.uuid == uuid) select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
).first() ).first()
user.password = password user.password = password
internal_username = user.internal_username internal_username = user.internal_username
+8
View File
@@ -0,0 +1,8 @@
from loon.web.users.service import sync_minecraft_users
async def players_sync_handler(client, userdata, msg, data):
if msg.topic != "loon/playerlist/sync":
return
await sync_minecraft_users(data.get("players", []))
+9 -3
View File
@@ -6,7 +6,8 @@ from sqlmodel import Session, select
from loon.web.db import engine from loon.web.db import engine
from loon.web.logs.service import write_log from loon.web.logs.service import write_log
from loon.web.logs.types import LogTypeEnum from loon.web.logs.types import LogTypeEnum
from loon.web.users.models import User from loon.web.users.models import MinecraftUser, User
from loon.web.users.service import sync_minecraft_users
async def register_request_handler(client, userdata, msg, data): async def register_request_handler(client, userdata, msg, data):
@@ -23,9 +24,14 @@ async def register_request_handler(client, userdata, msg, data):
password = data["password"] password = data["password"]
internal_username = data["internalUsername"] internal_username = data["internalUsername"]
username = data["username"]
await sync_minecraft_users([{"uuid": uuid, "username": username}])
with Session(engine) as session: with Session(engine) as session:
statement = select(User).where(User.uuid == uuid) minecraft_user = session.exec(select(MinecraftUser).where(MinecraftUser.uuid == uuid)).first()
statement = select(User).where(User.minecraft_user_id == minecraft_user.id)
user = session.exec(statement).first() user = session.exec(statement).first()
if user is not None: if user is not None:
@@ -41,7 +47,7 @@ async def register_request_handler(client, userdata, msg, data):
json.dumps({"success": False, "error": f"The {internal_username} is taken!"})) json.dumps({"success": False, "error": f"The {internal_username} is taken!"}))
return return
user = User(uuid=uuid, password=password, internal_username=internal_username, admin=False) user = User(minecraft_user_id=minecraft_user.id, password=password, internal_username=internal_username, admin=False)
session.add(user) session.add(user)
try: try:
session.commit() session.commit()
+4 -2
View File
@@ -3,7 +3,7 @@ import json
from sqlmodel import Session, select from sqlmodel import Session, select
from loon.web.db import engine from loon.web.db import engine
from loon.web.users.models import User from loon.web.users.models import MinecraftUser, User
async def show_username_handler(client, userdata, msg, data): async def show_username_handler(client, userdata, msg, data):
@@ -19,7 +19,9 @@ async def show_username_handler(client, userdata, msg, data):
return return
with Session(engine) as session: with Session(engine) as session:
user = session.exec(select(User).where(User.uuid == uuid)).first() user = session.exec(
select(User).join(MinecraftUser).where(MinecraftUser.uuid == uuid)
).first()
if user is None: if user is None:
client.publish(f"loon/auth/show_username/{uuid}/response", client.publish(f"loon/auth/show_username/{uuid}/response",
+4 -1
View File
@@ -9,6 +9,7 @@ from loon import logger
from loon.mqtt.handlers.change_password_request import change_password_handler from loon.mqtt.handlers.change_password_request import change_password_handler
from loon.mqtt.handlers.marquee import marquee_handler from loon.mqtt.handlers.marquee import marquee_handler
from loon.mqtt.handlers.player import player_handler from loon.mqtt.handlers.player import player_handler
from loon.mqtt.handlers.players_sync import players_sync_handler
from loon.mqtt.handlers.register_request import register_request_handler from loon.mqtt.handlers.register_request import register_request_handler
from loon.mqtt.handlers.server import server_handler from loon.mqtt.handlers.server import server_handler
from loon.mqtt.handlers.show_username_request import show_username_handler from loon.mqtt.handlers.show_username_request import show_username_handler
@@ -16,7 +17,7 @@ from loon.mqtt.handlers.world import world_handler
class MQTTManager: class MQTTManager:
BASE_HANDLERS = [player_handler, register_request_handler, world_handler, server_handler, marquee_handler, change_password_handler, show_username_handler] BASE_HANDLERS = [player_handler, register_request_handler, world_handler, server_handler, marquee_handler, change_password_handler, show_username_handler, players_sync_handler]
def __init__(self, inject_handlers = None): def __init__(self, inject_handlers = None):
if inject_handlers is None: if inject_handlers is None:
@@ -53,6 +54,8 @@ class MQTTManager:
logger.info(f"MQTT connected (rc={reason_code})") logger.info(f"MQTT connected (rc={reason_code})")
client.subscribe("loon/#") client.subscribe("loon/#")
client.publish("loon/playerlist/sync/request")
@staticmethod @staticmethod
def on_disconnect(client, userdata, reason_code): def on_disconnect(client, userdata, reason_code):
logger.warning(f"MQTT disconnected (rc={reason_code}). Reconnecting...") logger.warning(f"MQTT disconnected (rc={reason_code}). Reconnecting...")
+4 -1
View File
@@ -8,6 +8,7 @@ from starlette.authentication import (
) )
from starlette.requests import HTTPConnection from starlette.requests import HTTPConnection
from starlette.responses import JSONResponse, Response from starlette.responses import JSONResponse, Response
from sqlalchemy.orm import selectinload
from sqlmodel import Session, select from sqlmodel import Session, select
from loon.web.db import engine from loon.web.db import engine
@@ -62,7 +63,9 @@ class JWTAuthBackend(AuthenticationBackend):
raise AuthenticationError("Invalid or expired token") raise AuthenticationError("Invalid or expired token")
with Session(engine) as session: with Session(engine) as session:
user = session.exec(select(User).where(User.id == user_id)).first() user = session.exec(
select(User).where(User.id == user_id).options(selectinload(User.minecraft_user))
).first()
if user is None: if user is None:
raise AuthenticationError("User not found") raise AuthenticationError("User not found")
+16 -2
View File
@@ -1,9 +1,23 @@
from sqlmodel import SQLModel, Field from sqlmodel import SQLModel, Field, Relationship
class MinecraftUser(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
uuid: str = Field(unique=True)
username: str
user: "User" = Relationship(back_populates="minecraft_user")
class User(SQLModel, table=True): class User(SQLModel, table=True):
id: int = Field(default=None, primary_key=True) id: int = Field(default=None, primary_key=True)
uuid: str = Field(unique=True)
minecraft_user_id: int = Field(foreign_key="minecraftuser.id", nullable=False, unique=True)
minecraft_user: MinecraftUser = Relationship(back_populates="user")
internal_username: str = Field(unique=True) internal_username: str = Field(unique=True)
password: str password: str
admin: bool = Field(default=False) admin: bool = Field(default=False)
@property
def uuid(self) -> str:
return self.minecraft_user.uuid
+5 -1
View File
@@ -11,7 +11,11 @@ router = APIRouter(prefix="/users", tags=["users"])
@router.get("/me") @router.get("/me")
@authenticated @authenticated
async def me(request: Request): async def me(request: Request):
return request.user.user.model_dump(exclude={"password", "id"}) user = request.user.user
return {
**user.model_dump(exclude={"password", "id", "minecraft_user_id"}),
"minecraft_user": user.minecraft_user.model_dump(exclude={"id"}),
}
@router.post("/inventory/request") @router.post("/inventory/request")
@authenticated @authenticated
+43
View File
@@ -0,0 +1,43 @@
import asyncio
from sqlmodel import Session, select
from loon.web.db import engine
from loon.web.users.models import MinecraftUser
def _sync_minecraft_users(players: list[dict]) -> list[MinecraftUser]:
players_by_uuid = {player["uuid"]: player["username"] for player in players}
with Session(engine) as session:
existing = session.exec(
select(MinecraftUser).where(MinecraftUser.uuid.in_(players_by_uuid))
).all()
existing_by_uuid = {minecraft_user.uuid: minecraft_user for minecraft_user in existing}
new_users = []
for uuid, username in players_by_uuid.items():
minecraft_user = existing_by_uuid.get(uuid)
if minecraft_user is None:
minecraft_user = MinecraftUser(uuid=uuid, username=username)
new_users.append(minecraft_user)
session.add(minecraft_user)
elif minecraft_user.username != username:
minecraft_user.username = username
session.add(minecraft_user)
session.commit()
minecraft_users = [*existing, *new_users]
for minecraft_user in minecraft_users:
session.refresh(minecraft_user)
return minecraft_users
async def sync_minecraft_users(players: list[dict]) -> list[MinecraftUser]:
if not players:
return []
return await asyncio.to_thread(_sync_minecraft_users, players)