Compare commits

...

20 Commits

Author SHA1 Message Date
c0d608b63f Added save sorting by upload and progress 2023-01-26 11:25:42 +01:00
dd7031ff19 Changed DT method to fix UTC time 2023-01-25 15:59:54 +01:00
1294d1a037 Changed only_ids behavior + sorting added 2023-01-23 16:39:11 +01:00
546600a29e Changed only_ids response (still not final) 2023-01-23 10:36:28 +01:00
Profitroll
30f11a7c83 Added client arg to PATCH /devices/{name} 2023-01-22 22:35:33 +01:00
Profitroll
14d1ba9fa7 Added only_ids to GET /saves 2023-01-22 11:31:00 +01:00
Profitroll
ce768d895d Added getting the last save 2023-01-21 18:02:36 +01:00
daa3b0ca73 Added device client option 2023-01-20 16:02:46 +01:00
673c986ff9 Typo fixed 2023-01-19 22:15:07 +02:00
453c3e95dd Argument for /check fixed 2023-01-19 22:10:04 +02:00
aff3f76fc1 Local account support added 2023-01-19 15:31:40 +01:00
1ce8c0d712 Mailer added 2023-01-19 15:31:27 +01:00
5f8dff42c3 Saves unlim by -1 added 2023-01-19 14:13:14 +01:00
02552dce5a Limits implemented 2023-01-19 14:11:49 +01:00
84be0c7154 Versions added 2023-01-19 13:58:18 +01:00
08d060e160 Refactored some stuff 2023-01-19 13:47:18 +01:00
aa8be6006c Files zipping update 2023-01-19 13:35:27 +01:00
3c245d8671 Removed trash and optimized imports 2023-01-18 14:31:22 +01:00
335a497991 Added device arg into search by both ID 2023-01-18 14:30:14 +01:00
1beca94cc0 Added search by device 2023-01-18 14:27:58 +01:00
12 changed files with 214 additions and 145 deletions

View File

@@ -9,6 +9,11 @@
"locations": {
"data": "data"
},
"compression": 5,
"limits": {
"saves": -1,
"devices": -1
},
"messages": {
"key_expired": "API key expired",
"key_invalid": "Invalid API key",
@@ -19,5 +24,25 @@
"user_already_exists": "User with this username already exists.",
"email_confirmed": "Email confirmed. You can now log in.",
"email_code_invalid": "Confirmation code is invalid."
},
"external_address": "localhost",
"registration_enabled": false,
"registration_requires_confirmation": false,
"mailer": {
"smtp": {
"host": "",
"port": 0,
"sender": "",
"login": "",
"password": "",
"use_ssl": true,
"use_tls": false
},
"messages": {
"registration_confirmation": {
"subject": "Email confirmation",
"message": "To confirm your email please follow this link: {0}"
}
}
}
}

View File

@@ -3,7 +3,9 @@ from modules.database import col_devices, col_saves
from fastapi import HTTPException, Depends
from fastapi.responses import UJSONResponse, Response
from fastapi.openapi.models import APIKey
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT
from starlette.status import HTTP_204_NO_CONTENT, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT
from modules.utils import configGet
@app.get("/devices", response_class=UJSONResponse, description="Get all devices")
async def devices_get(apikey: APIKey = Depends(get_api_key)):
@@ -28,6 +30,7 @@ async def devices_get_by_name(name: str, apikey: APIKey = Depends(get_api_key)):
device = col_devices.find_one({"user": user_by_key(apikey), "name": name})
if device is not None:
del device["_id"]
del device["user"]
return UJSONResponse(device)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
@@ -44,19 +47,28 @@ async def devices_delete_by_name(name: str, apikey: APIKey = Depends(get_api_key
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
@app.post("/devices", response_class=UJSONResponse, description="Create new device")
async def devices_post(name: str, os: str, apikey: APIKey = Depends(get_api_key)):
async def devices_post(name: str, os: str, client: str, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
if (configGet("devices", "limits") != -1) and (col_devices.count_documents({"user": user}) >= configGet("devices", "limits")):
return UJSONResponse(
{
"detail": f'Too many devices. This instance allows to register only {configGet("devices", "limits")} devices per user',
"limit": configGet("devices", "limits")
},
status_code=HTTP_403_FORBIDDEN
)
if col_devices.find_one({"user": user, "name": name}) is not None:
raise HTTPException(HTTP_409_CONFLICT, detail="Device with this name already exists.")
col_devices.insert_one({"user": user, "name": name, "os": os, "last_save": 0})
col_devices.insert_one({"user": user, "name": name, "os": os, "client": client, "last_save": 0})
return Response(status_code=HTTP_204_NO_CONTENT)
@app.patch("/devices/{name}", description="Update name of the existing device")
async def devices_patch(name: str, new_name: str, os: str, apikey: APIKey = Depends(get_api_key)):
async def devices_patch(name: str, new_name: str, os: str, client: str, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
@@ -66,7 +78,7 @@ async def devices_patch(name: str, new_name: str, os: str, apikey: APIKey = Depe
if col_devices.find_one({"user": user, "name": new_name}) is not None:
raise HTTPException(HTTP_409_CONFLICT, detail="Device with this name already exists.")
col_devices.find_one_and_update({"user": user, "name": name}, {"$set": {"name": new_name, "os": os}})
col_devices.find_one_and_update({"user": user, "name": name}, {"$set": {"name": new_name, "os": os, "client": client}})
col_saves.update_many({"user": user, "device": name}, {"$set": {"device": new_name}})
return Response(status_code=HTTP_204_NO_CONTENT)

View File

@@ -1,74 +1,68 @@
from datetime import datetime
from io import BytesIO
from datetime import datetime, timezone
from urllib.parse import quote_plus
from os import path, remove
from typing import Dict, List, Union
from zipfile import ZipFile
from os import remove
from typing import Dict, List, Literal, Union
from xmltodict import parse
from models.saves import StardewSave
from pymongo import DESCENDING
from models.saves import StardewSave, StardewSaveBrief
from modules.app import app, get_api_key, user_by_key
from modules.utils import saveFile
from modules.database import col_devices, col_saves
from fastapi import HTTPException, Depends, UploadFile
from fastapi.responses import UJSONResponse, FileResponse, Response
from fastapi.openapi.models import APIKey
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE
from starlette.status import HTTP_204_NO_CONTENT, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE
from modules.utils import configGet, zip_saves
def zipfiles(filenames, save_name: str) -> Response:
@app.get("/saves", response_class=UJSONResponse, response_model=Union[List[Dict[str, StardewSave]], List[StardewSaveBrief]], description="Get all available game saves")
async def saves_get(device: Union[str, None] = None, version: Union[str, None] = None, only_ids: bool = False, sort: Union[Literal["upload", "progress"], None] = "upload", apikey: APIKey = Depends(get_api_key)):
zip_filename = save_name+".svsave"
user = user_by_key(apikey)
s = BytesIO()
zf = ZipFile(s, "w")
query = {"user": user}
for fpath in filenames:
if device is not None:
query["device"] = device
# Calculate path for file in zip
fdir, fname = path.split(fpath)
if version is not None:
query["data.game_version"] = version
# Add file, at correct path
for entry in (list(col_saves.find({"files.save.uuid": fname})) + list(col_saves.find({"files.saveinfo.uuid": fname}))):
filename = entry["files"]["save"]["name"] if (entry["files"]["save"]["uuid"] == fname) else entry["files"]["saveinfo"]["name"]
zf.write(fpath, filename)
# Must close zip for all contents to be written
zf.close()
# Grab ZIP file from in-memory, make response with correct MIME-type
return Response(
s.getvalue(),
media_type="application/x-zip-compressed",
headers={
'Content-Disposition': f'attachment;filename={quote_plus(zip_filename)}'
}
)
@app.get("/saves", response_class=UJSONResponse, response_model=Dict[str, StardewSave], description="Get all available game saves")
async def saves_get(apikey: APIKey = Depends(get_api_key)):
saves_entries = list(col_saves.find({"user": user_by_key(apikey)}))
saves_entries = list(col_saves.find(query).sort("date", DESCENDING)) if sort == "upload" else list(col_saves.find(query).sort("data.save_time", DESCENDING))
if len(saves_entries) == 0:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any saves.")
output = []
added = []
for entry in saves_entries:
out_entry = entry
del out_entry["_id"]
del out_entry["user"]
del out_entry["files"]
del out_entry["file"]
if only_ids is True:
if entry["id"] in added:
continue
else:
added.append(entry["id"])
output.append(out_entry)
return UJSONResponse(output)
@app.get("/saves/{id}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name")
async def saves_get_by_id(id: int, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
async def saves_get_by_id(id: int, device: Union[str, None] = None, version: Union[str, None] = None, sort: Union[Literal["upload", "progress"], None] = "upload", apikey: APIKey = Depends(get_api_key)):
saves_entries = list(col_saves.find({"id": id})) if device is None else list(col_saves.find({"id": id, "device": device}))
query = {"id": id}
if device is not None:
query["device"] = device
if version is not None:
query["data.game_version"] = version
saves_entries = list(col_saves.find(query).sort("date", DESCENDING)) if sort == "upload" else list(col_saves.find(query).sort("data.save_time", DESCENDING))
if len(saves_entries) == 0:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@@ -78,54 +72,67 @@ async def saves_get_by_id(id: int, device: Union[str, None] = None, apikey: APIK
for entry in saves_entries:
out_entry = entry
del out_entry["_id"]
del out_entry["files"]
del out_entry["file"]
del out_entry["user"]
output.append(out_entry)
return UJSONResponse(output)
@app.get("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name")
async def saves_get_by_both_ids(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date})
@app.get("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by id and upload date")
async def saves_get_by_both_ids(id: int, save_date: Union[int, Literal["latest"]], device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
query = {"user": user_by_key(apikey), "id": id, "date": save_date}
if device is not None:
query["device"] = device
saves_entry = col_saves.find_one(query)
if saves_entry is not None:
del saves_entry["_id"]
del saves_entry["files"]
del saves_entry["file"]
return UJSONResponse(saves_entry)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.delete("/saves/{id}", description="Get game saves by name")
@app.delete("/saves/{id}", description="Delete game saves by id")
async def saves_delete_by_id(id: int, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
if col_saves.count_documents({"user": user, "id": id}) > 0:
saves_entries = list(col_saves.find({"user": user, "id": id}))
for entry in saves_entries:
remove(entry["files"]["save"]["path"])
remove(entry["files"]["saveinfo"]["path"])
remove(entry["file"]["path"])
col_saves.delete_many({"user": user, "id": id})
return Response(status_code=HTTP_204_NO_CONTENT)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.delete("/saves/{id}/{save_date}", response_class=UJSONResponse, description="Get game saves by name")
@app.delete("/saves/{id}/{save_date}", response_class=UJSONResponse, description="Delete game saves by id and upload date")
async def saves_delete_by_both_ids(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one_and_delete({"id": id, "date": save_date})
if saves_entry is not None:
remove(saves_entry["files"]["save"]["path"])
remove(saves_entry["files"]["saveinfo"]["path"])
remove(saves_entry["file"]["path"])
return Response(status_code=HTTP_204_NO_CONTENT)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.get("/saves/{id}/{save_date}/download", response_class=FileResponse, description="Get game save as .svsave file by its id and save date")
async def saves_download(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date})
@app.get("/saves/{id}/{save_date}/download", response_class=FileResponse, description="Get game save as .svsave file by its id and upload date")
async def saves_download(id: int, save_date: int, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date}) if device is None else col_saves.find_one({"user": user_by_key(apikey), "id": id, "device": device, "date": save_date})
if saves_entry is not None: # type: ignore
return zipfiles([saves_entry["files"]["save"]["path"], saves_entry["files"]["saveinfo"]["path"]], save_name=f'{saves_entry["data"]["farmer"]}_{saves_entry["id"]}')
with open(saves_entry["file"]["path"], "rb") as file:
response = Response(
file.read(),
media_type="application/x-zip-compressed",
headers={
'Content-Disposition': f'attachment;filename={quote_plus(saves_entry["file"]["name"])}.svsave'
}
)
return response
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@@ -137,6 +144,15 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
error_return = HTTPException(HTTP_406_NOT_ACCEPTABLE, detail="You must provide two files: save file and SaveGameInfo for that save")
if (configGet("saves", "limits") != -1) and (col_saves.count_documents({"user": user}) >= configGet("saves", "limits")):
return UJSONResponse(
{
"detail": f'Too many save files. This instance allows to store only {configGet("saves", "limits")} saves per user',
"limit": configGet("saves", "limits")
},
status_code=HTTP_403_FORBIDDEN
)
if len(files) != 2:
return error_return
@@ -146,27 +162,27 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
if col_devices.find_one({"user": user, "name": device}) is None:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
save_info = save_data = save_info_file = save_data_filename = save_data_file = save_info_file_id = save_data_file_id = None
save_info = save_data = save_info_file = save_data_filename = save_data_file = None
for file in files:
if file.filename == "SaveGameInfo":
save_info_file = await file.read()
save_info_file_id = saveFile(save_info_file)
save_info = parse(save_info_file.decode("utf-8"))
if "Farmer" not in save_info:
return error_return
else:
save_data_filename = file.filename
save_data_file = await file.read()
save_data_file_id = saveFile(save_data_file)
save_data = parse(save_data_file.decode("utf-8"))
if "SaveGame" not in save_data:
return error_return
if save_info is None or save_data is None or save_info_file is None or save_data_filename is None or save_data_file is None or save_info_file_id is None or save_data_file_id is None:
if save_info is None or save_data is None or save_info_file is None or save_data_filename is None or save_data_file is None:
return error_return
save_date = int(datetime.utcnow().timestamp())
zipped = zip_saves(save_data_filename, save_data_file, save_info_file)
save_date = int(datetime.now(tz=timezone.utc).timestamp())
index = {
"id": int(save_data["SaveGame"]["uniqueIDForThisGame"]),
@@ -180,19 +196,13 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
"save_time": int(save_info["Farmer"]["saveTime"]),
"year": int(save_info["Farmer"]["yearForSaveGame"]),
"season": int(save_info["Farmer"]["seasonForSaveGame"]),
"day": int(save_info["Farmer"]["dayOfMonthForSaveGame"])
"day": int(save_info["Farmer"]["dayOfMonthForSaveGame"]),
"game_version": save_info["Farmer"]["gameVersion"]
},
"files": {
"save": {
"name": save_data_filename,
"uuid": save_data_file_id[0],
"path": save_data_file_id[1]
},
"saveinfo": {
"name": "SaveGameInfo",
"uuid": save_info_file_id[0],
"path": save_info_file_id[1]
}
"file": {
"name": save_data_filename,
"uuid": zipped[0],
"path": zipped[1]
}
}
@@ -201,7 +211,7 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
del save_info, save_data
del index["user"]
del index["files"]
del index["file"]
del index["_id"]
col_devices.find_one_and_update({"user": user, "name": device}, {"$set": {"last_save": save_date}})

View File

@@ -4,4 +4,5 @@ class Device(BaseModel):
user: str
name: str
os: str
client: str
last_save: int

View File

@@ -8,9 +8,14 @@ class StardewSaveData(BaseModel):
year: int
season: int
day: int
game_version: str
class StardewSave(BaseModel):
id: int
device: str
date: int
data: StardewSaveData
data: StardewSaveData
class StardewSaveBrief(BaseModel):
id: int
farmer: str

View File

@@ -1,4 +1,3 @@
from os import path
from typing import Union
from fastapi import FastAPI, Security, HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
@@ -6,7 +5,7 @@ from fastapi.security import APIKeyQuery, APIKeyHeader, APIKeyCookie
from fastapi.openapi.models import APIKey
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
from starlette.status import HTTP_401_UNAUTHORIZED
from modules.utils import configGet, jsonLoad
from modules.utils import configGet
from modules.security import passEncode
from modules.database import col_apikeys, col_expired
@@ -14,33 +13,11 @@ app = FastAPI(title="Stardew Sync", docs_url=None, redoc_url=None, version="0.1"
api_key_query = APIKeyQuery(name="apikey", auto_error=False)
api_key_header = APIKeyHeader(name="apikey", auto_error=False)
api_key_cookie = APIKeyCookie(name="apikey", auto_error=False)
def get_all_api_keys() -> list:
return jsonLoad(path.join(configGet("data", "locations"), "api_keys.json"))
def get_all_expired_keys() -> list:
return jsonLoad(path.join(configGet("data", "locations"), "expired_keys.json"))
# def check_project_key(project: str, apikey: APIKey) -> bool:
# keys = jsonLoad(path.join(configGet("data", "locations"), "api_keys.json"))
# if apikey in keys:
# if keys[apikey] != []:
# if project in keys[apikey]:
# return True
# else:
# return False
# else:
# return False
# else:
# return False
async def get_api_key(
api_key_query: str = Security(api_key_query),
api_key_header: str = Security(api_key_header),
api_key_cookie: str = Security(api_key_cookie),
) -> str:
def is_valid(key):
@@ -50,10 +27,8 @@ async def get_api_key(
return api_key_query
elif is_valid(api_key_header):
return api_key_header
elif is_valid(api_key_cookie):
return api_key_cookie
else:
if (col_expired.find_one({"hash": passEncode(api_key_query)}) is not None) or (col_expired.find_one({"hash": passEncode(api_key_header)}) is not None) or (col_expired.find_one({"hash": passEncode(api_key_cookie)}) is not None):
if (col_expired.find_one({"hash": passEncode(api_key_query)}) is not None) or (col_expired.find_one({"hash": passEncode(api_key_header)}) is not None):
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=configGet("key_expired", "messages"))
else:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages"))
@@ -62,6 +37,7 @@ def user_by_key(apikey: Union[str, APIKey]) -> Union[str, None]:
db_key = col_apikeys.find_one({"hash": passEncode(apikey)})
return db_key["user"] if db_key is not None else None
@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(

39
modules/mailer.py Normal file
View File

@@ -0,0 +1,39 @@
from smtplib import SMTP, SMTP_SSL
from traceback import print_exc
from ssl import create_default_context
from modules.utils import configGet, logWrite
try:
if configGet("use_ssl", "mailer", "smtp") is True:
mail_sender = SMTP_SSL(
configGet("host", "mailer", "smtp"),
configGet("port", "mailer", "smtp"),
)
logWrite(f"Initialized SMTP SSL connection")
elif configGet("use_tls", "mailer", "smtp") is True:
mail_sender = SMTP(
configGet("host", "mailer", "smtp"),
configGet("port", "mailer", "smtp"),
)
mail_sender.starttls(context=create_default_context())
mail_sender.ehlo()
logWrite(f"Initialized SMTP TLS connection")
else:
mail_sender = SMTP(
configGet("host", "mailer", "smtp"),
configGet("port", "mailer", "smtp")
)
mail_sender.ehlo()
logWrite(f"Initialized SMTP connection")
except Exception as exp:
logWrite(f"Could not initialize SMTP connection to: {exp}")
print_exc()
try:
mail_sender.login(
configGet("login", "mailer", "smtp"),
configGet("password", "mailer", "smtp")
)
logWrite(f"Successfully initialized mailer")
except Exception as exp:
logWrite(f"Could not login into provided SMTP account due to: {exp}")

View File

@@ -1,6 +1,7 @@
from os import makedirs, path
from typing import Any, Tuple, Union
from uuid import uuid4
from zipfile import ZIP_DEFLATED, ZipFile
from ujson import loads, dumps, JSONDecodeError
from traceback import print_exc
@@ -62,17 +63,24 @@ def configGet(key: str, *args: str) -> Any:
this_key = this_key[dict_key]
return this_key[key]
def saveFile(filebytes: bytes) -> Tuple[str, str]:
"""Save some bytedata into random file and return its ID
def zip_saves(save_filename: str, save_bytes: bytes, saveinfo_bytes: bytes) -> Tuple[str, str]:
"""Save files of the SV save into archive and return uuid and path
### Args:
* filebytes (`bytes`): Bytes to write into file
* save_filename (`str`): Filename of the save file
vsave_bytes (`bytes`): Bytes of the save file
* saveinfo_bytes (`bytes`): Bytes of the save info file
### Returns:
* `Tuple[str, str]`: Tuple where first item is an ID and the second is an absolute path to file
* `Tuple[str, str]`: First element is an UUID and the second is a filepath
"""
makedirs(path.join(configGet("data", "locations"), "files"), exist_ok=True)
filename = str(uuid4())
with open(path.join(configGet("data", "locations"), "files", filename), "wb") as file:
file.write(filebytes)
return filename, path.join(configGet("data", "locations"), "files", filename)
save_uuid = str(uuid4())
makedirs(path.join(configGet("data", "locations"), "files", save_uuid))
with ZipFile(path.join(configGet("data", "locations"), "files", save_uuid, save_filename+".svsave"), 'w', ZIP_DEFLATED, compresslevel=configGet("compression")) as ziph:
ziph.writestr("SaveGameInfo", saveinfo_bytes)
ziph.writestr(save_filename, save_bytes)
return save_uuid, path.join(configGet("data", "locations"), "files", save_uuid, save_filename+".svsave")

View File

@@ -9,7 +9,7 @@ makedirs(configGet("data", "locations"), exist_ok=True)
@app.get("/check", response_class=Response, include_in_schema=False)
async def check():
return Response(HTTP_200_OK)
return Response(status_code=HTTP_200_OK)
@app.get("/favicon.ico", response_class=FileResponse, include_in_schema=False)
async def favicon():

View File

@@ -11,6 +11,8 @@ parser = ArgumentParser(
)
parser.add_argument("-u", "--username", help="Enter username without input prompt", action="store")
parser.add_argument("-e", "--email", help="Enter email without input prompt", action="store")
parser.add_argument("-l", "--local", help="Do not save user's email to make it completely local and unrecoverable", action="store_true")
parser.add_argument("-j", "--json", help="Return output as a json. Username must be provided as an argument", action="store_true")
args = parser.parse_args()
@@ -18,8 +20,15 @@ args = parser.parse_args()
username = input("Enter username: ") if args.username is None else args.username
if args.local is False:
email = input("Enter email: ") if args.email is None else args.email
if email.strip() == "":
email = None
else:
email = None
new_key = str(uuid4())
col_apikeys.insert_one({"user": username, "hash": passEncode(new_key)})
col_apikeys.insert_one({"user": username, "email": email, "hash": passEncode(new_key)})
if args.json is True and args.username is not None:
print(dumps({"apikey": new_key}))

View File

@@ -4,6 +4,7 @@
"user",
"name",
"os",
"client",
"last_save"
],
"properties": {
@@ -16,6 +17,9 @@
"os": {
"bsonType": "string"
},
"client": {
"bsonType": "string"
},
"last_save": {
"bsonType": ["int", "double"]
}

View File

@@ -13,15 +13,10 @@
"data.year",
"data.season",
"data.day",
"files",
"files.save",
"files.save.name",
"files.save.uuid",
"files.save.path",
"files.saveinfo",
"files.saveinfo.name",
"files.saveinfo.uuid",
"files.saveinfo.path"
"file",
"file.name",
"file.uuid",
"file.path"
],
"properties": {
"id": {
@@ -60,31 +55,16 @@
"data.day": {
"bsonType": "int"
},
"files": {
"file": {
"bsonType": "object"
},
"files.save": {
"bsonType": "object"
},
"files.save.name": {
"file.name": {
"bsonType": "string"
},
"files.save.uuid": {
"file.uuid": {
"bsonType": "string"
},
"files.save.path": {
"bsonType": "string"
},
"files.saveinfo": {
"bsonType": "object"
},
"files.saveinfo.name": {
"bsonType": "string"
},
"files.saveinfo.uuid": {
"bsonType": "string"
},
"files.saveinfo.path": {
"file.path": {
"bsonType": "string"
}
}