diff --git a/Telegram_provider/main.py b/Telegram_provider/main.py
index d998cfe..a91aca7 100644
--- a/Telegram_provider/main.py
+++ b/Telegram_provider/main.py
@@ -14,7 +14,7 @@
import time
import asyncio
import io
-import aiohttp
+import fileuploader
"""Создание всех нужных объектов"""
load_dotenv()
@@ -30,25 +30,6 @@ class States(StatesGroup):
login_register = State()
-async def upload(bytes: bytearray, filename: str, user):
- form_data = aiohttp.FormData()
- form_data.add_field('file', bytes, filename=filename)
- headers = {}
- if user:
- headers = {"Authorization": "Bearer " + user.token}
-
- async with aiohttp.ClientSession("https://fu.andcool.ru") as session:
- async with session.post(f"/api/upload/private",
- data=form_data, headers=headers) as r:
- return r, await r.json()
-
-
-async def get_files(token: str):
- async with aiohttp.ClientSession("https://fu.andcool.ru") as session:
- async with session.get(f"/api/get_files/private", headers={"Authorization": "Bearer " + token}) as r:
- return r, await r.json()
-
-
async def send_login_message(message: types.Message):
builder = InlineKeyboardBuilder()
builder.add(types.InlineKeyboardButton(
@@ -85,26 +66,24 @@ async def account(message: types.Message, state: FSMContext):
return
else:
- response, response_json = await get_files(user.token)
- if response.status == 401:
+ try:
+ user_obj = await fileuploader.User.loginToken(user.token)
+ builder = InlineKeyboardBuilder()
+ builder.add(types.InlineKeyboardButton(
+ text="Log out",
+ callback_data=f"logout")
+ )
+
+ await message.answer(f"*Account:*\n*Username:* {user_obj.username}\n",
+ reply_markup=builder.as_markup(),
+ parse_mode="Markdown")
+ except fileuploader.exceptions.NotAuthorized:
await db.user.delete(where={"id": user.id})
await send_login_message(message)
return
-
- if response.status == 429:
+ except fileuploader.exceptions.TooManyRequests:
await message.answer("Sorry, the servers are overloaded right now")
return
-
- builder = InlineKeyboardBuilder()
- builder.add(types.InlineKeyboardButton(
- text="Log out",
- callback_data=f"logout")
- )
-
- await message.answer(f"*Account:*\n*Username:* {user.username}\n" + \
- f"*Files on account:* {len(response_json['data'])}",
- reply_markup=builder.as_markup(),
- parse_mode="Markdown")
@dp.callback_query(F.data.startswith("log_"))
@@ -125,45 +104,49 @@ async def log_reg(message: types.Message, state: FSMContext):
await message.answer('The data was sent incorrectly')
return
- async with aiohttp.ClientSession("https://fu.andcool.ru") as session:
- async with session.post(f"/api/{'login' if login_register == 'login' else 'register'}?bot=true",
- json={"username": login_and_password[0],
- "password": login_and_password[1]}) as r:
-
- await message.delete()
- data_resp = await r.json()
- if r.status == 400 or r.status == 404:
- await message.answer(data_resp['message'])
- return
-
- if r.status == 200:
- await db.user.create(data={
+ try:
+ if login_register == 'login':
+ user = await fileuploader.User.login(login_and_password[0], login_and_password[1])
+ else:
+ user = await fileuploader.User.register(login_and_password[0], login_and_password[1])
+
+ await db.user.create(data={
"user_id": message.from_user.id,
- "username": data_resp['username'],
- 'token': data_resp['accessToken']
- })
- await account(message, state)
- await state.clear()
- return
-
- await message.answer("Unhandled error")
- return
+ "username": user.username,
+ 'token': user.accessToken
+ })
+ await account(message, state)
+ await state.clear()
+ return
+ except fileuploader.exceptions.UserAreadyRegistered:
+ await message.answer("User with this username was already registered")
+ return
+ except fileuploader.exceptions.WrongPassword:
+ await message.answer("Wrong password")
+ return
+ except fileuploader.exceptions.UserNotFound:
+ await message.answer("User not found")
+ return
+ finally:
+ await message.delete()
@dp.callback_query(F.data == "logout")
async def log(callback: types.CallbackQuery, state: FSMContext):
- user = await db.user.find_first(where={"user_id": callback.from_user.id})
- if not user:
+ user_db = await db.user.find_first(where={"user_id": callback.from_user.id})
+ if not user_db:
await callback.message.answer("You are not logged in")
return
- async with aiohttp.ClientSession("https://fu.andcool.ru") as session:
- async with session.get(f"/api/logout", headers={"Authorization": "Bearer " + user.token}) as response:
- if response.status == 401 or response.status == 200:
- await db.user.delete(where={"id": user.id})
- await callback.message.answer("Logged out!")
- await callback.message.delete()
- return
+ user = fileuploader.User.User()
+ user.accessToken = user_db.token
+ try:
+ await user.logout()
+ finally:
+ await db.user.delete(where={"id": user_db.id})
+ await callback.message.answer("Logged out!")
+ await callback.message.delete()
+ return
@dp.message(F.content_type.in_({'document', 'photo', 'video', 'animation', 'video_note', 'voice', 'text'}))
@@ -235,22 +218,32 @@ async def send_file(message: types.Message, state: FSMContext):
filename = 'text.txt'
user = await db.user.find_first(where={"user_id": message.from_user.id})
- response, result = await upload(file_bytes, filename, user)
-
- if response.status != 200:
- error = result['error'] if response.status == 429 else result['message']
- await message.reply(error)
- return
+ user_obj = fileuploader.User.User(user.token)
- builder = InlineKeyboardBuilder()
- builder.add(types.InlineKeyboardButton(
- text="Delete file",
- callback_data=f"delete_{result['file_url']}_{result['key']}")
- )
+ try:
+ file = await fileuploader.upload(file_bytes, filename, user=user_obj)
+ builder = InlineKeyboardBuilder()
+ builder.add(types.InlineKeyboardButton(
+ text="Delete file",
+ callback_data=f"delete_{file.file_url}_{file.key}")
+ )
- await message.reply(f"*Your file has been uploaded.!*\n*Link:* {result['file_url_full']}\n*Size:* {result['size']}",
- reply_markup=builder.as_markup(),
- parse_mode="Markdown")
+ await message.reply(f"*Your file has been uploaded.!*\n*Link:* {file.file_url_full}\n*Size:* {file.size}",
+ reply_markup=builder.as_markup(),
+ parse_mode="Markdown")
+
+ except fileuploader.exceptions.FileSizeExceedsTheLimit:
+ await message.reply("File size exceeds the limit (100MB)")
+ return
+ except fileuploader.exceptions.InvalidGroup:
+ await message.reply("Invalid group")
+ return
+ except fileuploader.exceptions.GroupNotFound:
+ await message.reply("Group not found")
+ return
+ except fileuploader.exceptions.YouAreNotInTheGroup:
+ await message.reply("You are not in the group")
+ return
@dp.callback_query(F.data.startswith("delete_"))
@@ -258,15 +251,15 @@ async def delete_file(callback: types.CallbackQuery, state: FSMContext):
"""Хэндлер для колбэка кнопок выбора удаления"""
file_data = callback.data.replace("delete_", "").split("_")
- async with aiohttp.ClientSession("https://fu.andcool.ru") as session:
- async with session.get(f"/api/delete/{file_data[0]}?key={file_data[1]}") as r:
- response = await r.json()
- if response['status'] == 'success':
- await callback.message.delete()
- await callback.message.answer("File has been deleted!")
- else:
- await callback.message.answer(response['message'])
-
+ try:
+ await fileuploader.delete(file_data[0], file_data[1])
+ await callback.message.delete()
+ await callback.message.answer("File has been deleted!")
+
+ except fileuploader.exceptions.FileNotFound:
+ await callback.message.answer("File not found")
+ except fileuploader.exceptions.InvalidUniqueKey:
+ await callback.message.answer("Error while deleting file")
async def start():
"""Асинхронная функция для запуска диспатчера"""
diff --git a/accept_invite.html b/accept_invite.html
index 77c0621..f31ef97 100644
--- a/accept_invite.html
+++ b/accept_invite.html
@@ -115,6 +115,7 @@
Loading...
const host = window.location.host;
window.location.replace(protocol + "//" + host + page_url);
}
+
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
@@ -132,6 +133,17 @@ Loading...
return (parseInt(res["ExpiresAt"]) - 259200) > secondsSinceEpoch;
}
+ async function get_new_tokens(accessToken) {
+ try {
+ let response = await axios.post(api_url + "/api/refresh_token", { 'accessToken': "Bearer " + accessToken }, {})
+ if (!response) return false;
+ if (response.status != 200) return false;
+ return response.data.accessToken;
+ } catch {
+ return false;
+ }
+ }
+
async function fetch_info(invite_link) {
let accessToken = localStorage.getItem("accessToken");
if (!accessToken) moveToPage("/login");
diff --git a/imports.py b/imports.py
index fc02171..b69d962 100644
--- a/imports.py
+++ b/imports.py
@@ -19,5 +19,6 @@
import jwt
import bcrypt
import random
+import json
rate_limit_exceeded_handler = _rate_limit_exceeded_handler
diff --git a/main.py b/main.py
index 7dc0db2..7b7dc5b 100644
--- a/main.py
+++ b/main.py
@@ -58,7 +58,7 @@ async def api(request: Request):
)
-async def check_token(Authorization):
+async def check_token(Authorization, user_agent):
if not Authorization: # If token doesn't provided
return None, {"message": "No Authorization header provided", "errorId": -1}
@@ -86,6 +86,10 @@ async def check_token(Authorization):
if token["ExpiresAt"] < time.time(): # If token expired
await db.token.delete(where={"id": token_db.id})
return None, {"message": "Access token expired", "errorId": -3}
+
+ if user_agent != token_db.fingerprint and token_db.fingerprint != "None":
+ await db.token.delete(where={"id": token_db.id})
+ return None, {"message": "Invalid fingerprint", "errorId": -6}
return token_db, {}
@@ -104,9 +108,8 @@ async def upload_file(
request: Request,
include_ext: bool = False,
max_uses: int = 0,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
if not file: # Check, if the file is uploaded
@@ -132,7 +135,7 @@ async def upload_file(
saved_to_account = False
user_id = -1
- token_db, auth_error = await check_token(Authorization) # Check token
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token
if token_db: # If token is okay
saved_to_account = True
user_id = token_db.user.id
@@ -303,11 +306,10 @@ async def delete_file(url: str, key: str = ""):
async def getFiles(
group_id: str,
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -385,11 +387,19 @@ async def getFiles(
@app.post("/api/register") # Registartion handler
@limiter.limit(dynamic_limit_provider)
-async def register(request: Request, bot: bool = False):
- body = await request.json()
- if (
- "username" not in body or "password" not in body
- ): # If request body doesn't have username and password field
+async def register(request: Request, bot: bool = False, user_agent: Union[str, None] = Header(default=None)):
+ try:
+ body = await request.json()
+ except json.decoder.JSONDecodeError:
+ return JSONResponse(
+ {
+ "status": "error",
+ "message": "No username/password provided",
+ "errorId": 2,
+ },
+ status_code=400,
+ )
+ if ("username" not in body or "password" not in body): # If request body doesn't have username and password field
return JSONResponse(
{
"status": "error",
@@ -431,6 +441,7 @@ async def register(request: Request, bot: bool = False):
await db.token.create(
{ # Create token record in db
"accessToken": access,
+ "fingerprint": user_agent,
"user": {
"connect": {
"id": user.id,
@@ -449,10 +460,128 @@ async def register(request: Request, bot: bool = False):
)
+@app.get("/api/login/token") # login by token handler
+@limiter.limit(dynamic_limit_provider)
+async def login_token(request: Request,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)):
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
+ if not token_db: # If token is not valid
+ return JSONResponse(
+ content={
+ "status": "error",
+ "message": "Auth error",
+ "auth_error": auth_error,
+ },
+ status_code=401,
+ )
+ return JSONResponse({"status": "success",
+ "message": "Logged in by token",
+ "username": token_db.user.username,
+ "accessToken": token_db.accessToken})
+
+
+@app.post("/api/login/discord/{code}") # login handler
+@limiter.limit(dynamic_limit_provider)
+async def login(code: str,
+ request: Request,
+ user_agent: Union[str, None] = Header(default=None)):
+
+ data = {
+ 'grant_type': 'authorization_code',
+ 'code': code,
+ 'redirect_uri': "https://fu.andcool.ru/login/discord"
+ }
+ headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+
+ auth = aiohttp.BasicAuth(login=os.getenv("DISCORD_CLIENT_ID"), password=os.getenv("DISCORD_CLIENT_SECRET"))
+
+ response_json = {}
+ async with aiohttp.ClientSession() as session:
+ async with session.post(f'https://discord.com/api/v10/oauth2/token', data=data, headers=headers, auth=auth) as response:
+ response_json = await response.json()
+ if response.status != 200:
+ return JSONResponse({"status": "error", "message": "Internal error, please, log in again"}, status_code=401)
+
+ async with session.get('https://discord.com/api/users/@me',
+ headers={"Authorization": f"{response_json['token_type']} {response_json['access_token']}"}) as response_second:
+ if response_second.status != 200:
+ return JSONResponse({"status": "error", "message": "Invalid token, please, log in again"}, status_code=401)
+ response_user_json = await response_second.json()
+ user_check = await db.user.find_first(where={"discord_uid": response_user_json["id"]})
+
+ if not user_check:
+ user = await db.user.create( # Create user record in db
+ {"username": str(response_user_json["global_name"]), "password": "None", "discord_uid": response_user_json["id"]}
+ )
+ access = jwt.encode(
+ {
+ "user_id": int(user.id),
+ "ExpiresAt": time.time() + accesLifeTime,
+ },
+ "accessTokenSecret",
+ algorithm="HS256",
+ ) # Generate token
+
+ await db.token.create(
+ { # Create token record in db
+ "accessToken": access,
+ "fingerprint": user_agent,
+ "user": {
+ "connect": {
+ "id": user.id,
+ },
+ },
+ }
+ )
+ else:
+ access = jwt.encode(
+ {
+ "user_id": int(user_check.id),
+ "ExpiresAt": time.time() + accesLifeTime,
+ },
+ "accessTokenSecret",
+ algorithm="HS256",
+ ) # Generate token
+
+ await db.token.create(
+ { # Create token record in db
+ "accessToken": access,
+ "fingerprint": user_agent,
+ "user": {
+ "connect": {
+ "id": user_check.id,
+ },
+ },
+ }
+ )
+
+ return JSONResponse(
+ {
+ "status": "success",
+ "accessToken": access,
+ "username": response_user_json["global_name"],
+ "message": "registred" if not user_check else "logged in",
+ },
+ status_code=200,
+ )
+
@app.post("/api/login") # login handler
@limiter.limit(dynamic_limit_provider)
-async def login(request: Request, bot: bool = False):
- body = await request.json()
+async def login(request: Request, bot: bool = False, user_agent: Union[str, None] = Header(default=None)):
+ try:
+ body = await request.json()
+ except json.decoder.JSONDecodeError:
+ return JSONResponse(
+ {
+ "status": "error",
+ "message": "No username/password provided",
+ "errorId": 2,
+ },
+ status_code=400,
+ )
if ("username" not in body or "password" not in body): # If request body doesn't have username and password field
return JSONResponse(
{
@@ -474,7 +603,11 @@ async def login(request: Request, bot: bool = False):
)
user = await db.user.find_first(
- where={"username": body["username"]}, include={"tokens": True}
+ where={'AND': [
+ {"username": body["username"]},
+ {'NOT':[{"password": "None"}]}
+ ]
+ }, include={"tokens": True}
) # Find same username in db
if not user: # If user doesn't exists
return JSONResponse(
@@ -482,9 +615,7 @@ async def login(request: Request, bot: bool = False):
status_code=404,
)
- if bcrypt.checkpw(
- bytes(body["password"], "utf-8"), bytes(user.password, "utf-8")
- ): # If password is correct
+ if bcrypt.checkpw(bytes(body["password"], "utf-8"), bytes(user.password, "utf-8")): # If password is correct
access = jwt.encode(
{
"user_id": int(user.id),
@@ -500,6 +631,7 @@ async def login(request: Request, bot: bool = False):
await db.token.create(
{ # Create token record in db
"accessToken": access,
+ "fingerprint": user_agent,
"user": {
"connect": {
"id": user.id,
@@ -523,7 +655,7 @@ async def login(request: Request, bot: bool = False):
@app.post("/api/refresh_token") # refresh token handler
@limiter.limit(dynamic_limit_provider)
-async def refresh_token(request: Request):
+async def refresh_token(request: Request, user_agent: Union[str, None] = Header(default=None)):
body = await request.json()
if "accessToken" not in body: # If token doesn't provided
return JSONResponse(
@@ -531,9 +663,7 @@ async def refresh_token(request: Request):
status_code=400,
)
- token_db, auth_error = await check_token(
- body["accessToken"]
- ) # Check token validity
+ token_db, auth_error = await check_token(body["accessToken"], user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -560,12 +690,11 @@ async def refresh_token(request: Request):
@limiter.limit(dynamic_limit_provider)
async def logout(
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -585,12 +714,11 @@ async def logout(
@limiter.limit(dynamic_limit_provider)
async def transfer(
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -639,9 +767,8 @@ async def transfer(
@limiter.limit(dynamic_limit_provider)
async def create_group(
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
body = await request.json()
@@ -650,7 +777,7 @@ async def create_group(
{"status": "error", "message": "No `group_name` provided"}, status_code=400
)
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -692,12 +819,11 @@ async def create_group(
async def delete_group(
group_id: int,
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -733,12 +859,11 @@ async def delete_group(
async def generate_invite(
group_id: int,
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -780,12 +905,11 @@ async def generate_invite(
async def delete_group(
invite_link: str,
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -837,12 +961,11 @@ async def delete_group(
async def delete_group(
invite_link: str,
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -874,12 +997,11 @@ async def delete_group(
async def delete_group(
group_id: int,
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
@@ -915,12 +1037,11 @@ async def delete_group(
@limiter.limit(dynamic_limit_provider)
async def get_groups(
request: Request,
- Authorization: Annotated[
- Union[str, None], Header(convert_underscores=False)
- ] = None,
+ Authorization: Annotated[Union[str, None], Header(convert_underscores=False)] = None,
+ user_agent: Union[str, None] = Header(default=None)
):
- token_db, auth_error = await check_token(Authorization) # Check token validity
+ token_db, auth_error = await check_token(Authorization, user_agent) # Check token validity
if not token_db: # If token is not valid
return JSONResponse(
content={
diff --git a/schema.prisma b/schema.prisma
index 68477e8..079a3a6 100644
--- a/schema.prisma
+++ b/schema.prisma
@@ -29,16 +29,18 @@ model file {
}
model User {
- id Int @id @default(autoincrement())
- username String @default("")
- password String @default("")
- tokens Token[]
- groups Group[]
+ id Int @id @default(autoincrement())
+ username String @default("")
+ password String @default("")
+ discord_uid BigInt @default(-1)
+ tokens Token[]
+ groups Group[]
}
model Token{
id Int @id @default(autoincrement())
accessToken String @default("")
+ fingerprint String @default("None")
user User @relation(fields: [user_id], references: [id])
user_id Int @default(0)
}
diff --git a/web/login/discord/index.html b/web/login/discord/index.html
new file mode 100644
index 0000000..3239880
--- /dev/null
+++ b/web/login/discord/index.html
@@ -0,0 +1,141 @@
+
+
+
+
+ File uploader
+
+
+
+
+
+
+
+
+ File uploader
+
+
+
+
Loading...
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/login/index.html b/web/login/index.html
index e7fc368..80a11d5 100644
--- a/web/login/index.html
+++ b/web/login/index.html
@@ -42,6 +42,7 @@ Login
+ Login by Discord
@@ -51,6 +52,7 @@
Registration
+
Login by Discord