Skip to content

Commit fe266d2

Browse files
committed
Merge branch 'master' into kanban-board
2 parents 61a744f + 9b3453f commit fe266d2

64 files changed

Lines changed: 1545 additions & 775 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"class-transformer": "^0.5.1",
4747
"core-js": "^3.23.4",
4848
"dayjs": "^1.11.3",
49+
"file-saver": "^2.0.5",
4950
"fuse.js": "^6.6.2",
5051
"js-base64": "^3.7.7",
5152
"js-cookie": "^3.0.5",

projects/core/src/lib/interceptors/bearer-token.interceptor.ts

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ export class BearerTokenInterceptor implements HttpInterceptor {
4646
*/
4747
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
4848
// Базовые заголовки для всех запросов
49-
const headers: Record<string, string> = {
50-
Accept: "application/json",
51-
};
49+
const headers: Record<string, string> = {};
5250

5351
const tokens = this.tokenService.getTokens();
5452

@@ -57,14 +55,24 @@ export class BearerTokenInterceptor implements HttpInterceptor {
5755
headers["Authorization"] = `Bearer ${tokens.access}`;
5856
}
5957

58+
// Для blob запросов (файлы) не устанавливаем Accept, чтобы не парсить blob как JSON
59+
const isBlobRequest =
60+
request.url.includes("/export") ||
61+
request.url.includes("/download") ||
62+
(request.headers.has("X-Request-Type") && request.headers.get("X-Request-Type") === "blob");
63+
64+
const hasAcceptHeader = request.headers.has("Accept");
65+
66+
if (!isBlobRequest && !hasAcceptHeader) {
67+
headers["Accept"] = "application/json";
68+
}
69+
6070
const req = request.clone({ setHeaders: headers });
6171

62-
// Если токены есть, обрабатываем запрос с возможностью обновления токенов
6372
if (tokens !== null) {
6473
return this.handleRequestWithTokens(req, next);
6574
} else {
66-
// Если токенов нет, просто выполняем запрос
67-
return next.handle(request);
75+
return next.handle(req);
6876
}
6977
}
7078

@@ -107,10 +115,9 @@ export class BearerTokenInterceptor implements HttpInterceptor {
107115
request: HttpRequest<unknown>,
108116
next: HttpHandler
109117
): Observable<HttpEvent<unknown>> {
110-
// Если токен еще не обновляется, начинаем процесс обновления
111118
if (!this.isRefreshing) {
112119
this.isRefreshing = true;
113-
this.refreshTokenSubject.next(null); // Сбрасываем subject
120+
this.refreshTokenSubject.next(null);
114121

115122
return this.tokenService.refreshTokens().pipe(
116123
catchError(err => {
@@ -119,22 +126,29 @@ export class BearerTokenInterceptor implements HttpInterceptor {
119126
}),
120127
switchMap(res => {
121128
this.isRefreshing = false;
122-
this.refreshTokenSubject.next(res.access); // Уведомляем о новом токене
129+
this.refreshTokenSubject.next(res.access);
123130

124-
// Сохраняем новые токены в хранилище
125131
this.tokenService.memTokens(res);
126132

127-
// Подготавливаем заголовки с новым токеном
128-
const headers: Record<string, string> = {
129-
Accept: "application/json",
130-
};
133+
const headers: Record<string, string> = {};
131134

132135
const tokens = this.tokenService.getTokens();
133136
if (tokens) {
134137
headers["Authorization"] = `Bearer ${tokens.access}`;
135138
}
136139

137-
// Повторяем исходный запрос с новым токеном
140+
const isBlobRequest =
141+
request.url.includes("/export") ||
142+
request.url.includes("/download") ||
143+
(request.headers.has("X-Request-Type") &&
144+
request.headers.get("X-Request-Type") === "blob");
145+
146+
const hasAcceptHeader = request.headers.has("Accept");
147+
148+
if (!isBlobRequest && !hasAcceptHeader) {
149+
headers["Accept"] = "application/json";
150+
}
151+
138152
return next.handle(
139153
request.clone({
140154
setHeaders: headers,
@@ -144,17 +158,32 @@ export class BearerTokenInterceptor implements HttpInterceptor {
144158
);
145159
}
146160

147-
// Если токен уже обновляется, ждем завершения процесса
148161
return this.refreshTokenSubject.pipe(
149-
filter(token => token !== null), // Ждем получения нового токена
150-
take(1), // Берем только первое значение
151-
switchMap(token =>
152-
next.handle(
162+
filter(token => token !== null),
163+
take(1),
164+
switchMap(token => {
165+
const headers: Record<string, string> = {
166+
Authorization: `Bearer ${token}`,
167+
};
168+
169+
const isBlobRequest =
170+
request.url.includes("/export") ||
171+
request.url.includes("/download") ||
172+
(request.headers.has("X-Request-Type") &&
173+
request.headers.get("X-Request-Type") === "blob");
174+
175+
const hasAcceptHeader = request.headers.has("Accept");
176+
177+
if (!isBlobRequest && !hasAcceptHeader) {
178+
headers["Accept"] = "application/json";
179+
}
180+
181+
return next.handle(
153182
request.clone({
154-
setHeaders: { Authorization: `Bearer ${token}` },
183+
setHeaders: headers,
155184
})
156-
)
157-
)
185+
);
186+
})
158187
);
159188
}
160189
}

projects/core/src/lib/interceptors/camelcase.interceptor.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,34 @@ export class CamelcaseInterceptor implements HttpInterceptor {
5454
}),
5555
});
5656
} else {
57-
// Если тела нет, просто клонируем запрос
5857
req = request.clone();
5958
}
6059

6160
// Выполняем запрос и обрабатываем ответ
6261
return next.handle(req).pipe(
6362
map((event: HttpEvent<any>) => {
64-
// Обрабатываем только HTTP ответы (не события загрузки и т.д.)
6563
if (event instanceof HttpResponse) {
64+
if (event.body instanceof Blob) {
65+
return event;
66+
}
67+
68+
if (typeof event.body !== "object" || event.body === null) {
69+
return event;
70+
}
71+
6672
// Клонируем ответ с преобразованным телом (snake_case → camelCase)
67-
return event.clone({
68-
body: camelcaseKeys(event.body, {
69-
deep: true, // Рекурсивное преобразование вложенных объектов
70-
}),
71-
});
73+
try {
74+
return event.clone({
75+
body: camelcaseKeys(event.body, {
76+
deep: true, // Рекурсивное преобразование вложенных объектов
77+
}),
78+
});
79+
} catch (error) {
80+
console.warn("CamelcaseInterceptor: Failed to transform response body", error);
81+
return event;
82+
}
7283
}
7384

74-
// Для других типов событий возвращаем как есть
7585
return event;
7686
})
7787
);

projects/core/src/lib/services/api/api.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ export class ApiService {
4444
return this.http.get(this.apiUrl + path, { params, ...options }).pipe(first()) as Observable<T>;
4545
}
4646

47+
getFile(path: string, params?: HttpParams): Observable<Blob> {
48+
return this.http
49+
.get(this.apiUrl + path, {
50+
params,
51+
responseType: "blob",
52+
})
53+
.pipe(first()) as Observable<Blob>;
54+
}
55+
4756
/**
4857
* Выполняет PUT запрос к API (полное обновление ресурса)
4958
* @param path - Относительный путь к ресурсу

projects/skills/src/app/rating/general/general.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { SelectComponent } from "@ui/components";
1111
import { IconComponent } from "@uilib";
1212
import { FormBuilder, type FormGroup, ReactiveFormsModule } from "@angular/forms";
1313
import { RatingService } from "../services/rating.service";
14-
import { ratingFiltersList } from "projects/core/src/consts/filters/rating-filter.const";
14+
import { ratingFilters } from "projects/core/src/consts/filters/rating-filter.const";
1515

1616
/**
1717
* Компонент общего рейтинга пользователей
@@ -66,7 +66,7 @@ export class RatingGeneralComponent implements OnInit {
6666
ratingForm: FormGroup;
6767

6868
// Константы фильтров из конфигурации
69-
readonly filterParams = ratingFiltersList;
69+
readonly filterParams = ratingFilters;
7070

7171
/**
7272
* Инициализация компонента

projects/social_platform/src/app/api/auth/auth.service.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
RegisterResponse,
1212
} from "../../domain/auth/http.model";
1313
import { User, UserRole } from "../../domain/auth/user.model";
14+
import { Project } from "../../domain/project/project.model";
15+
import { ApiPagination } from "../../domain/other/api-pagination.model";
1416

1517
/**
1618
* Сервис аутентификации и управления пользователями
@@ -76,12 +78,8 @@ export class AuthService {
7678
.pipe(map(json => plainToInstance(RegisterResponse, json)));
7779
}
7880

79-
/**
80-
* Отправить резюме по email
81-
* @returns Observable завершения операции
82-
*/
83-
sendCV(): Observable<any> {
84-
return this.apiService.get(`${this.AUTH_USERS_URL}/send_mail_cv/`);
81+
downloadCV() {
82+
return this.apiService.getFile(`${this.AUTH_USERS_URL}/download_cv/`);
8583
}
8684

8785
/** Поток данных профиля пользователя */
@@ -127,6 +125,14 @@ export class AuthService {
127125
);
128126
}
129127

128+
/**
129+
* Получить проекты где пользователь leader
130+
* @returns Observable проектов внутри профиля
131+
*/
132+
getLeaderProjects(): Observable<ApiPagination<Project>> {
133+
return this.apiService.get(`${this.AUTH_USERS_URL}/projects/leader/`);
134+
}
135+
130136
/**
131137
* Получить роли, которые может изменить текущий пользователь
132138
* @returns Observable с массивом изменяемых ролей

projects/social_platform/src/app/api/program/program.service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PartnerProgramFields } from "projects/social_platform/src/app/domain/pr
1010
import { Program, ProgramDataSchema } from "../../domain/program/program.model";
1111
import { ProgramCreate } from "../../domain/program/program-create.model";
1212
import { Project } from "../../domain/project/project.model";
13+
import { ProjectAdditionalFields } from "../../domain/project/project-additional-fields.model";
1314

1415
/**
1516
* Сервис для работы с программами
@@ -67,6 +68,10 @@ export class ProgramService {
6768
return this.apiService.get(`${this.PROGRAMS_URL}/`, httpParams);
6869
}
6970

71+
getActualPrograms(): Observable<ApiPagination<Program>> {
72+
return this.apiService.get(`${this.PROGRAMS_URL}/`);
73+
}
74+
7075
getOne(programId: number): Observable<Program> {
7176
return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/`);
7277
}
@@ -103,6 +108,15 @@ export class ProgramService {
103108
return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/filters/`);
104109
}
105110

111+
getProgramProjectAdditionalFields(programId: number): Observable<ProjectAdditionalFields> {
112+
return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/projects/apply/`);
113+
}
114+
115+
// body - это форма проекта который подается + programFieldValues
116+
applyProjectToProgram(programId: number, body: any): Observable<any> {
117+
return this.apiService.post(`${this.PROGRAMS_URL}/${programId}/projects/apply/`, body);
118+
}
119+
106120
createProgramFilters(
107121
programId: number,
108122
filters: Record<string, string[]>,

projects/social_platform/src/app/api/project/project-additional.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export class ProjectAdditionalService {
242242
control.addValidators([Validators.maxLength(500)]);
243243
break;
244244
case "textarea":
245-
control.addValidators([Validators.maxLength(300)]);
245+
control.addValidators([Validators.maxLength(500)]);
246246
break;
247247
}
248248
}

0 commit comments

Comments
 (0)