diff --git a/auth/auth.gen.go b/auth/auth.gen.go index b24deb5..adb2b06 100644 --- a/auth/auth.gen.go +++ b/auth/auth.gen.go @@ -116,9 +116,9 @@ type PostAuthSignInResponseObject interface { } type PostAuthSignIn200JSONResponse struct { - Error *string `json:"error"` - UserId *string `json:"user_id"` - UserName *string `json:"user_name"` + Error *string `json:"error"` + Success *bool `json:"success,omitempty"` + UserId *string `json:"user_id"` } func (response PostAuthSignIn200JSONResponse) VisitPostAuthSignInResponse(w http.ResponseWriter) error { diff --git a/auth/openapi-auth.yaml b/auth/openapi-auth.yaml index 0fe308c..913c000 100644 --- a/auth/openapi-auth.yaml +++ b/auth/openapi-auth.yaml @@ -59,23 +59,29 @@ paths: type: string format: password responses: - # This one also sets two cookies: access_token and refresh_token "200": description: Sign-in result with JWT + # headers: + # Set-Cookie: + # schema: + # type: array + # items: + # type: string + # explode: true + # style: simple content: application/json: schema: type: object properties: + success: + type: boolean error: type: string nullable: true user_id: type: string nullable: true - user_name: - type: string - nullable: true "401": description: Access denied due to invalid credentials content: diff --git a/modules/auth/handlers/handlers.go b/modules/auth/handlers/handlers.go index 7f675aa..9b9b0d3 100644 --- a/modules/auth/handlers/handlers.go +++ b/modules/auth/handlers/handlers.go @@ -78,6 +78,7 @@ func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInReque } err := "" + success := true pass, ok := UserDb[req.Body.Nickname] if !ok || pass != req.Body.Pass { @@ -95,9 +96,9 @@ func (s Server) PostAuthSignIn(ctx context.Context, req auth.PostAuthSignInReque // Return access token; refresh token can be returned in response or HttpOnly cookie result := auth.PostAuthSignIn200JSONResponse{ - Error: &err, - UserId: &req.Body.Nickname, - UserName: &req.Body.Nickname, + Error: &err, + Success: &success, + UserId: &req.Body.Nickname, } return result, nil } diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx index 3ecfa2d..5a25313 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -1,34 +1,23 @@ import React from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; -import UsersIdPage from "./pages/UsersIdPage/UsersIdPage"; +import UserPage from "./pages/UserPage/UserPage"; import TitlesPage from "./pages/TitlesPage/TitlesPage"; import { LoginPage } from "./pages/LoginPage/LoginPage"; import { Header } from "./components/Header/Header"; const App: React.FC = () => { - // Получаем username из localStorage - const username = localStorage.getItem("username") || undefined; - const userId = localStorage.getItem("userId"); - + const username = "nihonium"; return (
- } /> - } /> - - {/* /profile рендерит UsersIdPage с id из localStorage */} - : } - /> - - } /> + } /> {/* <-- маршрут для логина */} + } /> {/* <-- можно использовать тот же компонент для регистрации */} + } /> } /> ); }; - export default App; \ No newline at end of file diff --git a/modules/frontend/src/api/core/OpenAPI.ts b/modules/frontend/src/api/core/OpenAPI.ts index 185e5c3..6ce873e 100644 --- a/modules/frontend/src/api/core/OpenAPI.ts +++ b/modules/frontend/src/api/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: '/api/v1', + BASE: 'http://10.1.0.65:8081/api/v1', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts index bb42012..52321b8 100644 --- a/modules/frontend/src/api/services/DefaultService.ts +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -20,7 +20,7 @@ export class DefaultService { * @param sort * @param sortForward * @param word - * @param status List of title statuses to filter + * @param status * @param rating * @param releaseYear * @param releaseSeason @@ -35,7 +35,7 @@ export class DefaultService { sort?: TitleSort, sortForward: boolean = true, word?: string, - status?: Array, + status?: TitleStatus, rating?: number, releaseYear?: number, releaseSeason?: ReleaseSeason, @@ -125,112 +125,45 @@ export class DefaultService { }, }); } - /** - * Partially update a user account - * Update selected user profile fields (excluding password). - * Password updates must be done via the dedicated auth-service (`/auth/`). - * Fields not provided in the request body remain unchanged. - * - * @param userId User ID (primary key) - * @param requestBody - * @returns User User updated successfully. Returns updated user representation (excluding sensitive fields). - * @throws ApiError - */ - public static updateUser( - userId: number, - requestBody: { - /** - * ID of the user avatar (references `images.id`); set to `null` to remove avatar - */ - avatar_id?: number | null; - /** - * User email (must be unique and valid) - */ - mail?: string; - /** - * Username (alphanumeric + `_` or `-`, 3–16 chars) - */ - nickname?: string; - /** - * Display name - */ - disp_name?: string; - /** - * User description / bio - */ - user_desc?: string; - }, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'PATCH', - url: '/users/{user_id}', - path: { - 'user_id': userId, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 400: `Invalid input (e.g., validation failed, nickname/email conflict, malformed JSON)`, - 401: `Unauthorized — missing or invalid authentication token`, - 403: `Forbidden — user is not allowed to modify this resource (e.g., not own profile & no admin rights)`, - 404: `User not found`, - 409: `Conflict — e.g., requested \`nickname\` or \`mail\` already taken by another user`, - 422: `Unprocessable Entity — semantic errors not caught by schema (e.g., invalid \`avatar_id\`)`, - 500: `Unknown server error`, - }, - }); - } /** * Get user titles * @param userId * @param cursor - * @param sort - * @param sortForward * @param word - * @param status List of title statuses to filter + * @param status * @param watchStatus * @param rating - * @param myRate * @param releaseYear * @param releaseSeason * @param limit * @param fields - * @returns any List of user titles + * @returns UserTitle List of user titles * @throws ApiError */ public static getUsersTitles( userId: string, cursor?: string, - sort?: TitleSort, - sortForward: boolean = true, word?: string, - status?: Array, - watchStatus?: Array, + status?: TitleStatus, + watchStatus?: UserTitleStatus, rating?: number, - myRate?: number, releaseYear?: number, releaseSeason?: ReleaseSeason, limit: number = 10, fields: string = 'all', - ): CancelablePromise<{ - data: Array; - cursor: CursorObj; - }> { + ): CancelablePromise> { return __request(OpenAPI, { method: 'GET', - url: '/users/{user_id}/titles', + url: '/users/{user_id}/titles/', path: { 'user_id': userId, }, query: { 'cursor': cursor, - 'sort': sort, - 'sort_forward': sortForward, 'word': word, 'status': status, 'watch_status': watchStatus, 'rating': rating, - 'my_rate': myRate, 'release_year': releaseYear, 'release_season': releaseSeason, 'limit': limit, @@ -242,43 +175,4 @@ export class DefaultService { }, }); } - /** - * Add a title to a user - * User adding title to list af watched, status required - * @param userId ID of the user to assign the title to - * @param requestBody - * @returns any Title successfully added to user - * @throws ApiError - */ - public static addUserTitle( - userId: number, - requestBody: UserTitle, - ): CancelablePromise<{ - data?: { - user_id: number; - title_id: number; - status: UserTitleStatus; - rate?: number; - review_id?: number; - ctime?: string; - }; - }> { - return __request(OpenAPI, { - method: 'POST', - url: '/users/{user_id}/titles', - path: { - 'user_id': userId, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 400: `Invalid request body (missing fields, invalid types, etc.)`, - 401: `Unauthorized — missing or invalid auth token`, - 403: `Forbidden — user not allowed to assign titles to this user`, - 404: `User or Title not found`, - 409: `Conflict — title already assigned to user (if applicable)`, - 500: `Internal server error`, - }, - }); - } } diff --git a/modules/frontend/src/auth/services/AuthService.ts b/modules/frontend/src/auth/services/AuthService.ts index 94578d8..bab9c77 100644 --- a/modules/frontend/src/auth/services/AuthService.ts +++ b/modules/frontend/src/auth/services/AuthService.ts @@ -41,9 +41,9 @@ export class AuthService { pass: string; }, ): CancelablePromise<{ + success?: boolean; error?: string | null; user_id?: string | null; - user_name?: string | null; }> { return __request(OpenAPI, { method: 'POST', diff --git a/modules/frontend/src/pages/LoginPage/LoginPage.tsx b/modules/frontend/src/pages/LoginPage/LoginPage.tsx index 89ee88c..dcd6965 100644 --- a/modules/frontend/src/pages/LoginPage/LoginPage.tsx +++ b/modules/frontend/src/pages/LoginPage/LoginPage.tsx @@ -18,19 +18,17 @@ export const LoginPage: React.FC = () => { try { if (isLogin) { const res = await AuthService.postAuthSignIn({ nickname, pass: password }); - if (res.user_id && res.user_name) { - // Сохраняем user_id и username в localStorage - localStorage.setItem("userId", res.user_id); - localStorage.setItem("username", res.user_name); - - navigate("/profile"); // редирект на профиль + if (res.success) { + // TODO: сохранить JWT в localStorage/cookie + console.log("Logged in user id:", res.user_id); + navigate("/"); // редирект после успешного входа } else { setError(res.error || "Login failed"); } } else { - // SignUp оставляем без сохранения данных const res = await AuthService.postAuthSignUp({ nickname, pass: password }); - if (res.user_id) { + if (res.success) { + console.log("User signed up with id:", res.user_id); setIsLogin(true); // переключаемся на login после регистрации } else { setError(res.error || "Sign up failed"); diff --git a/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx b/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx deleted file mode 100644 index b5a8336..0000000 --- a/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx +++ /dev/null @@ -1,183 +0,0 @@ -// pages/UserPage/UserPage.tsx -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import { DefaultService } from "../../api/services/DefaultService"; -import { SearchBar } from "../../components/SearchBar/SearchBar"; -import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox"; -import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; -import { ListView } from "../../components/ListView/ListView"; -import { TitleCardSquare } from "../../components/cards/TitleCardSquare"; -import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"; -import type { User, UserTitle, CursorObj, TitleSort } from "../../api"; - -const PAGE_SIZE = 10; - -type UsersIdPageProps = { - userId?: string; -}; - -export default function UsersIdPage({ userId }: UsersIdPageProps) { - const params = useParams(); - const id = userId || params?.id; - - const [user, setUser] = useState(null); - const [loadingUser, setLoadingUser] = useState(true); - const [errorUser, setErrorUser] = useState(null); - - // Для списка тайтлов - const [titles, setTitles] = useState([]); - const [nextPage, setNextPage] = useState([]); - const [cursor, setCursor] = useState(null); - const [loadingTitles, setLoadingTitles] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [search, setSearch] = useState(""); - const [sort, setSort] = useState("id"); - const [sortForward, setSortForward] = useState(true); - const [layout, setLayout] = useState<"square" | "horizontal">("square"); - - // --- Получение данных пользователя --- - useEffect(() => { - const fetchUser = async () => { - if (!id) return; - setLoadingUser(true); - try { - const result = await DefaultService.getUsers(id, "all"); - setUser(result); - setErrorUser(null); - } catch (err: any) { - console.error(err); - setErrorUser(err?.message || "Failed to fetch user data"); - } finally { - setLoadingUser(false); - } - }; - fetchUser(); - }, [id]); - - // --- Получение списка тайтлов пользователя --- - const fetchPage = async (cursorObj: CursorObj | null) => { - if (!id) return { items: [], nextCursor: null }; - const cursorStr = cursorObj - ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") - : ""; - - try { - const result = await DefaultService.getUsersTitles( - id, - cursorStr, - sort, - sortForward, - search.trim() || undefined, - undefined, // status фильтр, можно добавить - undefined, // watchStatus - undefined, // rating - undefined, // myRate - undefined, // releaseYear - undefined, // releaseSeason - PAGE_SIZE, - "all" - ); - - if (!result?.data?.length) return { items: [], nextCursor: null }; - - return { items: result.data, nextCursor: result.cursor ?? null }; - } catch (err: any) { - if (err.status === 204) return { items: [], nextCursor: null }; - throw err; - } - }; - - // Инициализация: загружаем сразу две страницы - useEffect(() => { - const initLoad = async () => { - setLoadingTitles(true); - setTitles([]); - setNextPage([]); - setCursor(null); - - const firstPage = await fetchPage(null); - const secondPage = firstPage.nextCursor ? await fetchPage(firstPage.nextCursor) : { items: [], nextCursor: null }; - - setTitles(firstPage.items); - setNextPage(secondPage.items); - setCursor(secondPage.nextCursor); - setLoadingTitles(false); - }; - initLoad(); - }, [id, search, sort, sortForward]); - - const handleLoadMore = async () => { - if (nextPage.length === 0) { - setLoadingMore(false); - return; - } - setLoadingMore(true); - - setTitles(prev => [...prev, ...nextPage]); - setNextPage([]); - - if (cursor) { - try { - const next = await fetchPage(cursor); - if (next.items.length > 0) setNextPage(next.items); - setCursor(next.nextCursor); - } catch (err) { - console.error(err); - } - } - - setLoadingMore(false); - }; - - const getAvatarUrl = (avatarId?: number) => (avatarId ? `/api/images/${avatarId}` : "/default-avatar.png"); - - return ( -
- - {/* --- Карточка пользователя --- */} - {loadingUser &&
Loading user...
} - {errorUser &&
{errorUser}
} - {user && ( -
- {user.nickname} -

{user.disp_name || user.nickname}

- {user.mail &&

{user.mail}

} - {user.user_desc &&

{user.user_desc}

} - {user.creation_date &&

Registered: {new Date(user.creation_date).toLocaleDateString()}

} -
- )} - - {/* --- Панель поиска, сортировки и лейаута --- */} -
- - - -
- - {/* --- Список тайтлов --- */} - {loadingTitles &&
Loading titles...
} - {!loadingTitles && titles.length === 0 &&
No titles found.
} - - {titles.length > 0 && ( - <> - - items={titles} - layout={layout} - hasMore={!!cursor || nextPage.length > 1} - loadingMore={loadingMore} - onLoadMore={handleLoadMore} - renderItem={(title, layout) => - layout === "square" ? : - } - /> - - {!cursor && nextPage.length === 0 && ( -
- Результатов больше нет, было найдено {titles.length} тайтлов. -
- )} - - )} -
- ); -}