diff --git a/YakPanel-server/backend/app/core/__pycache__/__init__.cpython-314.pyc b/YakPanel-server/backend/app/core/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 00000000..287f1216 Binary files /dev/null and b/YakPanel-server/backend/app/core/__pycache__/__init__.cpython-314.pyc differ diff --git a/YakPanel-server/backend/app/core/__pycache__/security.cpython-314.pyc b/YakPanel-server/backend/app/core/__pycache__/security.cpython-314.pyc new file mode 100644 index 00000000..739f1c4a Binary files /dev/null and b/YakPanel-server/backend/app/core/__pycache__/security.cpython-314.pyc differ diff --git a/YakPanel-server/backend/scripts/seed_admin.py b/YakPanel-server/backend/scripts/seed_admin.py index b46b8bfc..f704d4bc 100644 --- a/YakPanel-server/backend/scripts/seed_admin.py +++ b/YakPanel-server/backend/scripts/seed_admin.py @@ -14,12 +14,19 @@ from app.core.security import get_password_hash from app.models.user import User -async def seed(): +async def seed(*, reset_password: bool = False): await init_db() async with AsyncSessionLocal() as db: result = await db.execute(select(User).where(User.username == "admin")) - if result.scalar_one_or_none(): - print("Admin user already exists") + existing = result.scalar_one_or_none() + if existing: + if reset_password: + existing.password = get_password_hash("admin") + existing.is_active = True + await db.commit() + print("Admin password reset: username=admin, password=admin") + else: + print("Admin user already exists (use --reset-password to force admin/admin)") return admin = User( username="admin", @@ -32,4 +39,5 @@ async def seed(): if __name__ == "__main__": - asyncio.run(seed()) + reset = "--reset-password" in sys.argv + asyncio.run(seed(reset_password=reset)) diff --git a/YakPanel-server/frontend/src/api/client.ts b/YakPanel-server/frontend/src/api/client.ts index 2f1adef1..fb4a5b89 100644 --- a/YakPanel-server/frontend/src/api/client.ts +++ b/YakPanel-server/frontend/src/api/client.ts @@ -1,5 +1,20 @@ const API_BASE = '/api/v1' +function formatFastApiDetail(detail: unknown): string | undefined { + if (typeof detail === 'string') return detail + if (Array.isArray(detail)) { + return detail + .map((item) => { + if (item && typeof item === 'object' && 'msg' in item) { + return String((item as { msg: string }).msg) + } + return JSON.stringify(item) + }) + .join('; ') + } + return undefined +} + export async function apiRequest( path: string, options: RequestInit = {} @@ -26,16 +41,26 @@ export async function apiRequest( } export async function login(username: string, password: string) { - const form = new FormData() - form.append('username', username) - form.append('password', password) + // OAuth2 password flow uses application/x-www-form-urlencoded (RFC 6749). + // multipart FormData can be mishandled by some proxies and is non-standard here. + const body = new URLSearchParams() + body.set('username', username) + body.set('password', password) const res = await fetch(`${API_BASE}/auth/login`, { method: 'POST', - body: form, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), }) if (!res.ok) { - const err = await res.json().catch(() => ({})) - throw new Error(err.detail || `Login failed`) + const err = await res.json().catch(() => null) + const detail = err ? formatFastApiDetail(err.detail) : undefined + const msg = + detail || + (err && typeof (err as { message?: string }).message === 'string' + ? (err as { message: string }).message + : undefined) || + `Login failed (${res.status})` + throw new Error(msg) } const data = await res.json() localStorage.setItem('token', data.access_token)