diff --git a/docs/backend-web-android-parity.md b/docs/backend-web-android-parity.md new file mode 100644 index 0000000..59b82c7 --- /dev/null +++ b/docs/backend-web-android-parity.md @@ -0,0 +1,55 @@ +# Backend/Web/Android Parity Snapshot (2026-03-09) + +## 1) Backend vs Web + +Backend покрывает web-функционал почти полностью (`~95-100%`): + +- `auth`: login/refresh/me, register, verify-email, resend verification, request/reset password, sessions, 2FA +- `chats`: list/detail, saved, discover, create/join/leave, members/bans, title/profile, pin/archive, invite-link, notifications, clear/delete +- `messages`: list/send/edit/delete, status, search/thread, forward/bulk, reactions +- `media`: upload-url, attachments create/list +- `realtime`: websocket + typing/read/delivered/ping-pong +- `users`: search/profile/blocked/contacts +- `search`: global search +- `notifications`: list + +Вывод: текущие проблемы в основном на стороне клиентской интеграции/UX, не backend-contract. + +## 2) Web endpoints not yet fully used on Android + +- `GET /api/v1/chats/saved` +- `PATCH /api/v1/chats/{chat_id}/title` +- `PATCH /api/v1/chats/{chat_id}/profile` +- `POST /api/v1/chats/{chat_id}/archive` +- `POST /api/v1/chats/{chat_id}/unarchive` +- `POST /api/v1/chats/{chat_id}/pin-chat` +- `POST /api/v1/chats/{chat_id}/unpin-chat` +- `DELETE /api/v1/chats/{chat_id}` +- `POST /api/v1/chats/{chat_id}/clear` +- `GET /api/v1/chats/{chat_id}/notifications` +- `PUT /api/v1/chats/{chat_id}/notifications` +- `GET /api/v1/messages/{message_id}/thread` +- `GET /api/v1/search` (single global endpoint; Android uses composed search calls) +- Contacts endpoints: + - `GET /api/v1/users/contacts` + - `POST /api/v1/users/{user_id}/contacts` + - `POST /api/v1/users/contacts/by-email` + - `DELETE /api/v1/users/{user_id}/contacts` +- `GET /api/v1/notifications` +- `POST /api/v1/auth/resend-verification` + +## 3) Practical status + +- Backend readiness vs Web: `high` +- Android parity vs Web (feature-level): `~70-80%` + +## 4) Highest-priority Android parity step + +Подключить в Android реальные действия для chats list popup/select: + +- archive/unarchive +- pin/unpin chat +- delete/clear chat +- chat notification settings + +и перевести текущие UI-заглушки на API-вызовы. diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index 14b3cb0..bab118a 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -14,7 +14,7 @@ export async function requestPasswordResetRequest(email: string): Promise } export async function resetPasswordRequest(token: string, password: string): Promise { - await http.post("/auth/reset-password", { token, password }); + await http.post("/auth/reset-password", { token, new_password: password }); } export async function loginRequest(email: string, password: string, otpCode?: string, recoveryCode?: string): Promise { diff --git a/web/src/api/http.ts b/web/src/api/http.ts index 1506c56..a17de78 100644 --- a/web/src/api/http.ts +++ b/web/src/api/http.ts @@ -21,7 +21,16 @@ let refreshInFlight: Promise | null = null; function shouldSkipRefresh(config?: InternalAxiosRequestConfig): boolean { const url = config?.url ?? ""; - return url.includes("/auth/login") || url.includes("/auth/refresh"); + return ( + url.includes("/auth/login") || + url.includes("/auth/refresh") || + url.includes("/auth/register") || + url.includes("/auth/check-email") || + url.includes("/auth/verify-email") || + url.includes("/auth/resend-verification") || + url.includes("/auth/request-password-reset") || + url.includes("/auth/reset-password") + ); } http.interceptors.response.use( diff --git a/web/src/app/App.tsx b/web/src/app/App.tsx index f40f8f1..70eaf51 100644 --- a/web/src/app/App.tsx +++ b/web/src/app/App.tsx @@ -76,8 +76,11 @@ export function App() { return; } window.localStorage.setItem(PENDING_RESET_PASSWORD_TOKEN_KEY, resetToken); + if (accessToken) { + logout(); + } window.history.replaceState(null, "", "/"); - }, []); + }, [accessToken, logout]); useEffect(() => { const nav = extractNotificationNavigationFromLocation(); @@ -221,10 +224,28 @@ function extractPasswordResetTokenFromLocation(): string | null { return null; } const url = new URL(window.location.href); - if (!/^\/reset-password\/?$/i.test(url.pathname)) { + if (!/^\/reset-password(?:\/[^/]+)?\/?$/i.test(url.pathname)) { return null; } - return url.searchParams.get("token")?.trim() || null; + const tokenFromQuery = + url.searchParams.get("token")?.trim() || + url.searchParams.get("reset_token")?.trim(); + if (tokenFromQuery) { + return tokenFromQuery; + } + const pathMatch = url.pathname.match(/^\/reset-password\/([^/]+)\/?$/i); + if (pathMatch?.[1]?.trim()) { + return pathMatch[1].trim(); + } + if (url.hash) { + const hash = url.hash.replace(/^#/, ""); + const hashParams = new URLSearchParams(hash); + const tokenFromHash = hashParams.get("token")?.trim() || hashParams.get("reset_token")?.trim(); + if (tokenFromHash) { + return tokenFromHash; + } + } + return null; } function inviteJoinErrorMessage(error: unknown): string {