diff --git a/config_example.json b/config_example.json new file mode 100644 index 0000000..049814b --- /dev/null +++ b/config_example.json @@ -0,0 +1,16 @@ +{ + "locations": { + "data": "data" + }, + "messages": { + "key_expired": "API key expired", + "key_invalid": "Invalid API key", + "key_valid": "Valid API key", + "bad_request": "Bad request. Read the docs at photos.end-play.xyz/docs", + "ip_blacklisted": "Your IP is blacklisted. Make sure you are using correct API address.", + "credentials_invalid": "Incorrect user or password", + "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." + } +} \ No newline at end of file diff --git a/data/api_keys.json b/data/api_keys.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/api_keys.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/data/expired_keys.json b/data/expired_keys.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/data/expired_keys.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..ecea693 Binary files /dev/null and b/favicon.ico differ diff --git a/modules/app.py b/modules/app.py new file mode 100644 index 0000000..02184d0 --- /dev/null +++ b/modules/app.py @@ -0,0 +1,75 @@ +from os import path +from fastapi import FastAPI, Security, HTTPException +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from fastapi.security import APIKeyQuery, APIKeyHeader, APIKeyCookie +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 + +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: + + keys = get_all_api_keys() + expired = get_all_expired_keys() + + def is_valid(key): + return True if key in keys else False + + if is_valid(api_key_query): + 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 (api_key_query in expired) or (api_key_header in expired) or (api_key_cookie in expired): + 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")) + +@app.get("/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url=app.openapi_url, # type: ignore + title=app.title + " - Documentation", + swagger_favicon_url="/favicon.ico" + ) + +@app.get("/redoc", include_in_schema=False) +async def custom_redoc_html(): + return get_redoc_html( + openapi_url=app.openapi_url, # type: ignore + title=app.title + " - Documentation", + redoc_favicon_url="/favicon.ico" + ) \ No newline at end of file diff --git a/modules/extensions_loader.py b/modules/extensions_loader.py new file mode 100644 index 0000000..49f8159 --- /dev/null +++ b/modules/extensions_loader.py @@ -0,0 +1,47 @@ +from importlib.util import module_from_spec, spec_from_file_location +from os import getcwd, path, walk + +#================================================================================= + +# Import functions +# Took from https://stackoverflow.com/a/57892961 +def get_py_files(src): + cwd = getcwd() + py_files = [] + for root, dirs, files in walk(src): + for file in files: + if file.endswith(".py"): + py_files.append(path.join(cwd, root, file)) + return py_files + + +def dynamic_import(module_name, py_path): + try: + module_spec = spec_from_file_location(module_name, py_path) + module = module_from_spec(module_spec) # type: ignore + module_spec.loader.exec_module(module) # type: ignore + return module + except SyntaxError: + print(f"Could not load extension {module_name} due to invalid syntax. Check logs/errors.log for details.", flush=True) + return + except Exception as exp: + print(f"Could not load extension {module_name} due to {exp}", flush=True) + return + + +def dynamic_import_from_src(src, star_import = False): + my_py_files = get_py_files(src) + for py_file in my_py_files: + module_name = path.split(py_file)[-1][:-3] + print(f"Importing {module_name} extension...", flush=True) + imported_module = dynamic_import(module_name, py_file) + if imported_module != None: + if star_import: + for obj in dir(imported_module): + globals()[obj] = imported_module.__dict__[obj] + else: + globals()[module_name] = imported_module + print(f"Successfully loaded {module_name} extension", flush=True) + return + +#================================================================================= \ No newline at end of file diff --git a/modules/utils.py b/modules/utils.py new file mode 100644 index 0000000..e50fcf9 --- /dev/null +++ b/modules/utils.py @@ -0,0 +1,61 @@ +from typing import Any, Union +from ujson import loads, dumps, JSONDecodeError +from traceback import print_exc + +# Print to stdout and then to log +def logWrite(message: str, debug: bool = False) -> None: + # save to log file and rotation is to be done + # logAppend(f'{message}', debug=debug) + print(f"{message}", flush=True) + +def jsonLoad(filepath: str) -> Any: + """Load json file + + ### Args: + * filepath (`str`): Path to input file + + ### Returns: + * `Any`: Some json deserializable + """ + with open(filepath, "r", encoding='utf8') as file: + try: + output = loads(file.read()) + except JSONDecodeError: + logWrite(f"Could not load json file {filepath}: file seems to be incorrect!\n{print_exc()}") + raise + except FileNotFoundError: + logWrite(f"Could not load json file {filepath}: file does not seem to exist!\n{print_exc()}") + raise + file.close() + return output + +def jsonSave(contents: Union[list, dict], filepath: str) -> None: + """Save contents into json file + + ### Args: + * contents (`Union[list, dict]`): Some json serializable + * filepath (`str`): Path to output file + """ + try: + with open(filepath, "w", encoding='utf8') as file: + file.write(dumps(contents, ensure_ascii=False, indent=4)) + file.close() + except Exception as exp: + logWrite(f"Could not save json file {filepath}: {exp}\n{print_exc()}") + return + +def configGet(key: str, *args: str) -> Any: + """Get value of the config key + + ### Args: + * key (`str`): The last key of the keys path. + * *args (`str`): Path to key like: dict[args][key]. + + ### Returns: + * `Any`: Value of provided key + """ + this_dict = jsonLoad("config.json") + this_key = this_dict + for dict_key in args: + this_key = this_key[dict_key] + return this_key[key] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..076a6cd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi[all] +ujson~=5.7.0 +pydantic~=1.10.4 +apscheduler~=3.9.1.post1 \ No newline at end of file diff --git a/sync_api.py b/sync_api.py new file mode 100644 index 0000000..9a65c4a --- /dev/null +++ b/sync_api.py @@ -0,0 +1,25 @@ +from os import makedirs, path +from modules.app import app +from modules.utils import configGet +from modules.extensions_loader import dynamic_import_from_src +from fastapi.responses import FileResponse + +makedirs(configGet("data", "locations"), exist_ok=True) + +for entry in [path.join(configGet("data", "locations"), "api_keys.json"), path.join(configGet("data", "locations"), "expired_keys.json")]: + mode = 'r' if path.exists(entry) else 'w' + with open(entry, mode) as f: + try: + f.write("[]") + except: + pass + + +@app.get("/favicon.ico", response_class=FileResponse, include_in_schema=False) +async def favicon(): + return FileResponse("favicon.ico") + + +#================================================================================= +dynamic_import_from_src("extensions", star_import = True) +#================================================================================= \ No newline at end of file