Skip to content

Commit

Permalink
code formatting and start docs dev
Browse files Browse the repository at this point in the history
  • Loading branch information
Andcool-Systems committed Sep 20, 2024
1 parent a3ca228 commit 7fef88a
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 97 deletions.
72 changes: 68 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
# PPLBandage Backend
[![Made with Prisma](https://made-with.prisma.io/indigo.svg)](https://prisma.io)
![Lines Count](https://img.shields.io/endpoint?url=https%3A%2F%2Fghloc.vercel.app%2Fapi%2FPPLBandage%2FPPLBandage_Backend%2Fbadge%3Ffilter%3D.ts%24%2C.tsx%24%2C.css%24)
[![Made for pepeland](https://andcool.ru/static/badges/made-for-ppl.svg)](https://pepeland.net)
<p align="left">
<img src="https://made-with.prisma.io/indigo.svg" alt='prisma'></img>
<img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fghloc.vercel.app%2Fapi%2FPPLBandage%2FPPLBandage_Backend%2Fbadge%3Ffilter%3D.ts%24%2C.tsx%24%2C.css%24" alt='prisma'></img>
<img src="https://andcool.ru/static/badges/made-for-ppl.svg" alt='pepeland'></img>
</p>

На данный момент документация к публичному API сайта недоступна.
## Оглавление
- [Base URL](#base-url)
- [Ограничения по частоте запросов](#ограничения-по-частоте-запросов)
- [Ошибки](#ошибки)
- [Аутентификация](#аутентификация)
- Документация к API
- - [Корневые эндпоинты API](/docs/mainRoute.md)

## Введение
**API PPLBandage — это RestFul API, позволяющий приложениям взаимодействовать с серверами и базой данных проекта.**

## Base URL
`https://pplbandage.ru/api`

## Ограничения по частоте запросов
Глобальное ограничение на все эндпоинты: `150rpm`, однако оно может варьироваться в зависимости от конечного эндпоинта.
| Эндпоинт | Ограничение по частоте запросов |
| ---------------------------------------------------- | ------------------------------- |
| `GET /api/ping` | Нет |
| `GET /api/user/me/connections/minecraft/cache/purge` | `5rpm` |
| `POST /api/workshop` | `5rpm` |
| `GET /api/workshop/:id` | Нет |
| `GET /api/workshop/:id/info` | Нет |
Отсутствие ограничения по количеству запросов на эндпоинтах означает, что они являются закрытыми и для доступа к ним нужен специальный ключ. Об этом подробнее в описании эндпоинтов.

## Ошибки
Всего может возникнуть несколько типов ошибок:
### Ошибки валидации
Ошибки валидации могут возникнуть, если в тело запроса или query параметры было передано неверное значение.
```json
{
"message": [
"Массив ошибок валидации"
],
"error": "Описание кода ошибки",
"statusCode": 400
}
```

### Общие ошибки
Общие ошибки могут возникнуть во всех остальных случаях, например, если пользователь не авторизован.
```json
{
"status": "error",
"message": "UNAUTHORIZED",
"statusCode": 401
}
```
Так же в ошибках может быть поле `message_ru`, содержащее описание ошибки на русском.

## Аутентификация
Аутентификация происходит через **токены сессии**.
### Описание процесса аутентификации
Токены сессии должны передаваться в cookies запроса по ключу `sessionId`.
При создании запроса к API он проверяет валидность токена, время жизни которого равно `14 дням` или двум неделям. Если со времени создания токена прошло более половины от его времени жизни, то бекенд обновляет его и отправляет клиенту в `Setcookie` хедере.
> [!NOTE]
> Обычно, все эндпоинты, требующие строгой аутентификации всегда отправляют `Setcookie` хедер, содержащий актуальный в данный момент токен сессии. В случае, когда токен был обновлён, он так же отправляется в хедерах. Клиенты должны при каждом запросе устанавливать актуальный токен из запроса.
> [!WARNING]
> Бекенд **не поддерживает** выполнение конкурентных запросов при обновлении токена. Клиенты должны сами позаботиться, чтобы запросы, подразумевающие обновление токена не выполнялись параллельно. В ином случае есть риск потерять текущую сессию.
Если время жизни токена закончилось, бекенд вернёт HTTP код `401`.
Все сессии связываются с fingerprint'ом текущего клиента, который представлен `User-Agent` хедером. Если запрос делается с другого `User-Agent` и по этому же токену, то он автоматически удаляется, возвращая HTTP код `401`.
35 changes: 35 additions & 0 deletions docs/mainRoute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Корневые эндпоинты API
**Здесь располагаются все эндпоинты, являющиеся общими и не относящиеся ни к одному из частей бекенда.**


## `GET /api`
Основной эндпоинт API, делает redirect с кодом `308` в корень URL (`/`).

## `GET /ping`
Делает ping запрос на сервер. Не имеет ограничений по частоте запросов. Максимально быстро возвращает ответ от сервера с кодом `200`

### Тело ответа
```json
{
"statusCode": 200,
"message": "pong"
}
```

## `GET /sitemap.xml`
`Content-Type: text/xml`
Возвращает актуальную карту сайта в формате `xml` для индекса поисковых систем. Содержит основные страницы сайта, а так же все публичные повязки и профили авторов.

### Пример карты сайта
```xml
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://pplbandage.ru/</loc>
<priority>1</priority>
</url>
<url>
<loc>https://pplbandage.ru/workshop</loc>
<priority>0.8</priority>
</url>
</urlset>
```
14 changes: 4 additions & 10 deletions src/common/bandage_response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,9 @@ export interface BandageFull extends Bandage {
export const generate_response = (data: BandageFull[], session: Session | null, suppress_ban?: boolean) => {
/* generate list of works response by provided array of bandages */

const result = data.map((el) => {
const result = data.map(el => {
if (el.User?.UserSettings?.banned && !suppress_ban) return undefined;
const categories = el.categories.map((cat) => {
return {
id: cat.id,
name: cat.name,
icon: cat.icon
}
})
const categories = el.categories.map(cat => ({ id: cat.id, name: cat.name, icon: cat.icon }))
return {
id: el.id,
external_id: el.externalId,
Expand All @@ -51,9 +45,9 @@ export const generate_response = (data: BandageFull[], session: Session | null,
username: el.User?.username,
public: el.User && Number(el.User?.discordId) > 0 ? el.User?.UserSettings?.public_profile : false
},
categories: categories.filter((el) => el !== undefined)
categories: categories.filter(el => el !== undefined)
}
});

return result.filter((el) => el !== undefined);
return result.filter(el => el !== undefined);
}
3 changes: 1 addition & 2 deletions src/guards/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,12 @@ export class AuthGuard implements CanActivate {
}

request.session = session;
if (session && strict === 'Strict') {
if (session) {
response.setHeader('Access-Control-Expose-Headers', 'SetCookie');
response.setHeader('SetCookie', session.cookie);
}

session && await this.prisma.user.update({ where: { id: session.user.id }, data: { last_accessed: new Date() } })

return true;
}
}
11 changes: 0 additions & 11 deletions src/interfaces/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ interface Profile {
properties: Properties[];
}


interface SearchUnit {
name: string,
uuid: string,
Expand All @@ -43,16 +42,6 @@ interface Search {
next_page: number
}

interface SearchQuery {
take?: string,
page?: string,
search?: string,
for_edit?: string,
filters?: string,
sort?: string,
state?: string
}

interface SearchParams {
fragment: string,
take: number,
Expand Down
4 changes: 3 additions & 1 deletion src/minecraft/minecraft.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export class minecraftController {
throw new HttpException({
status: 'error',
message: 'Profile not found'
}, HttpStatus.NOT_FOUND);
},
HttpStatus.NOT_FOUND
);
}

res.setHeader('Content-Type', 'image/png');
Expand Down
19 changes: 11 additions & 8 deletions src/minecraft/minecraft.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class MinecraftService {
if (!response_uuid || response_uuid?.status !== 200) {
return null;
}
uuid = response_uuid?.data.id;
uuid = response_uuid.data.id;
}
const response_skin = await axios.get(
`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`,
Expand All @@ -31,7 +31,7 @@ export class MinecraftService {
if (!response_skin || response_skin?.status !== 200) {
return null;
}
return response_skin?.data;
return response_skin.data;
}


Expand All @@ -48,7 +48,7 @@ export class MinecraftService {
if (!response_uuid || response_uuid?.status !== 200) {
return null;
}
uuid = response_uuid?.data.id;
uuid = response_uuid.data.id;
}
return uuid;
}
Expand Down Expand Up @@ -77,7 +77,10 @@ export class MinecraftService {
.resize(36, 36, { kernel: sharp.kernel.nearest })
.png()
.toBuffer();
head.composite([{ input: firstLayer, top: 2, left: 2, blend: 'over' }, { input: secondLayer, top: 0, left: 0, blend: 'over' }]);
head.composite([
{ input: firstLayer, top: 2, left: 2, blend: 'over' },
{ input: secondLayer, top: 0, left: 0, blend: 'over' }
]);

return await head.toBuffer();
}
Expand All @@ -87,7 +90,7 @@ export class MinecraftService {
/* resolve nicknames collisions in data base */

for (const record of profiles) {
const data = await this.getUserData(record?.uuid);
const data = await this.getUserData(record.uuid);
if (!data) {
continue;
}
Expand Down Expand Up @@ -133,15 +136,15 @@ export class MinecraftService {
})
}

const nicks = await this.prisma.minecraft.findMany({ where: { nickname: fetched_skin_data?.name.toLowerCase() } });
if (nicks.length > 1) {
const profiles = await this.prisma.minecraft.findMany({ where: { nickname: fetched_skin_data?.name.toLowerCase() } });
if (profiles.length > 1) {
/* -- resolve nicknames collision --
Since the cache of skins and nicknames is not deleted after they expire,
there is a possibility that Minecraft accounts will be occupied by other
nicknames and there will be a name collision in the database
*/

await this.resolveCollisions(nicks);
await this.resolveCollisions(profiles);
}

const textures = Buffer.from(fetched_skin_data.properties[0].value, 'base64').toString();
Expand Down
2 changes: 1 addition & 1 deletion src/notifications/notifications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class NotificationService {
const notification_db = await this.prisma.notifications.create({ data: notification });
const users = await this.prisma.user.findMany();

users.forEach((user) => {
users.forEach(user => {
this.prisma.user.update({
where: { id: user.id },
data: {
Expand Down
12 changes: 4 additions & 8 deletions src/oauth/oauth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const generateCookie = (session: string, exp: number): string => {

export const hasAccess = (user: UserFull | undefined, level: number) => {
if (!user) return false;
const user_roles = user.AccessRoles.map((role) => role.level);
const user_roles = user.AccessRoles.map(role => role.level);
return user_roles.includes(level) || user_roles.includes(RolesEnum.SuperAdmin);
}

Expand Down Expand Up @@ -109,13 +109,9 @@ export class OauthService {
return false;
}
const data = response.data as PepelandResponse;
const roles = (await this.getRoles()).map((role) => role.ds_id);
for (const role of data.roles) {
if (roles.includes(role)) {
return true;
}
}
return false;
const roles = (await this.getRoles()).map(role => role.ds_id);

return data.roles.some(role => roles.includes(role));
}

async resolveCollisions(username: string) {
Expand Down
14 changes: 2 additions & 12 deletions src/root/root.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import { generateSitemap, SitemapProps } from './sitemap';
import { PrismaService } from 'src/prisma/prisma.service';
import { AuthGuard } from 'src/guards/auth.guard';
import { RolesGuard } from 'src/guards/roles.guard';
import { Auth } from 'src/decorators/auth.decorator';
import { AuthEnum, RolesEnum } from 'src/interfaces/types';
import { Roles } from 'src/decorators/access.decorator';


export const UNAUTHORIZED = {
Expand Down Expand Up @@ -57,7 +54,7 @@ export class RootController {
]

const bandages = await this.prisma.bandage.findMany({ where: { access_level: 2 } });
urls = urls.concat(bandages.map((bandage) => ({
urls = urls.concat(bandages.map(bandage => ({
loc: `https://pplbandage.ru/workshop/${bandage.externalId}`,
priority: 0.6
})));
Expand All @@ -69,18 +66,11 @@ export class RootController {
UserSettings: { banned: false, public_profile: true }
}
});
urls = urls.concat(users.map((user) => ({
urls = urls.concat(users.map(user => ({
loc: `https://pplbandage.ru/users/${user.username}`,
priority: 0.5
})));

return generateSitemap(urls);
}

@Get('/test')
@Auth(AuthEnum.Strict)
@Roles([RolesEnum.SuperAdmin])
async test() {
return { 'statusCode': 200, 'message': 'Access granted' }
}
}
Loading

0 comments on commit 7fef88a

Please sign in to comment.