Compare commits
108 Commits
ec5d0585a2
...
master
Author | SHA1 | Date | |
---|---|---|---|
00af3433e9 | |||
31da6b29ca | |||
2884d989a8 | |||
ae984a20e5 | |||
c62359c9c2 | |||
560ef5c986 | |||
f0587d0422 | |||
381c6fd0d3 | |||
b8a0ff79ec | |||
51253d82af | |||
6cdc804d30 | |||
fe4cd2dc48 | |||
b7def0fe64 | |||
921119ee75 | |||
bebf1f8541 | |||
16afeb5a95 | |||
764dfe540c | |||
df32ee393e | |||
936a86728a | |||
0438bd0483 | |||
ff3a18691b | |||
5a3e2fffe9 | |||
f248433e11 | |||
8c26ffc7be | |||
8a7f53d63b | |||
2c2704cd91 | |||
1a7fe795b4 | |||
d01842913d | |||
50c629c157 | |||
95bfb1fe2f | |||
b53743fb19 | |||
2103d6b003 | |||
126070bfdc | |||
b660a12e23 | |||
e1bb3e9af9 | |||
8a58902f8c | |||
8ba45faf50 | |||
78f24bc423 | |||
a148b9ac30 | |||
af936b522d | |||
50da0f78c7 | |||
bda26f5ebf | |||
3df80337b7 | |||
6955f1860a | |||
22818c4cae | |||
7e5f6c3cb7 | |||
46b6ed0e22 | |||
89cdd8ca3b | |||
50dfccef1a | |||
a84cd1c5ed | |||
2830a8e21a | |||
eee7e3a73f | |||
0de1f7f08d | |||
5227c7cd14 | |||
6fea31e3fb | |||
e8312a99d0 | |||
ab8b9bacde | |||
296b0014d5 | |||
adcf25ff15 | |||
508984442b | |||
37a6b4634c | |||
5796c6ea40 | |||
b488bbee0a | |||
43597db03c | |||
e6f3b7c4b1 | |||
e91a6ad10e | |||
aa6c5b99b1 | |||
0991734377 | |||
d4aa6558d6 | |||
186fddacef | |||
bf006a0734 | |||
9d70724d64 | |||
8b30afdf6a | |||
0db4661658 | |||
bf4fbe2302 | |||
3bb24f786d | |||
7adf849150 | |||
e6c0a53742 | |||
fa09dbc9b2 | |||
c3a9a2f40a | |||
58933a9279 | |||
bcf74089f9 | |||
891dc81271 | |||
25c902c194 | |||
5a794f7dc6 | |||
848b2f1a8e | |||
539b3b42c9 | |||
f01d2d177b | |||
5129cb449e | |||
4d6efac3c4 | |||
88b820e90d | |||
afefea6f68
|
|||
e5fad5ba92
|
|||
5174602c31
|
|||
0043abdbad
|
|||
0f423166f1
|
|||
b2146b965a | |||
3aa171869b | |||
126c66637e | |||
d0d127d9c0 | |||
728917b4b9 | |||
b1eb8f9aac | |||
0a30512dbc | |||
14b09d7062 | |||
ac8f2b2ba6 | |||
eab19e6783 | |||
8347a4c779 | |||
1bcca0f812 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -153,5 +153,6 @@ cython_debug/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
# Custom
|
# Custom
|
||||||
.vscode
|
data/
|
||||||
|
.vscode/
|
||||||
config.json
|
config.json
|
24
README.md
24
README.md
@@ -1,7 +1,7 @@
|
|||||||
<h1 align="center">Photos API</h1>
|
<h1 align="center">Photos API</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://git.end-play.xyz/profitroll/PhotosAPILICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
|
<a href="https://git.end-play.xyz/profitroll/PhotosAPI/src/branch/master/README.md"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
|
||||||
<a href="https://git.end-play.xyz/profitroll/PhotosAPI"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
<a href="https://git.end-play.xyz/profitroll/PhotosAPI"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@ First you need to have a Python interpreter, MongoDB and optionally git. You can
|
|||||||
3. Create virtual environment [Optional yet recommended]:
|
3. Create virtual environment [Optional yet recommended]:
|
||||||
|
|
||||||
1. Install virtualenv module: `pip install virtualenv`
|
1. Install virtualenv module: `pip install virtualenv`
|
||||||
2. Create venv: `python -m venv env`
|
2. Create venv: `python -m venv .venv`
|
||||||
3. Activate it using `source venv/bin/activate` on Linux, `venv\Scripts\activate.bat` in CMD or `venv\Scripts\Activate.ps1` in PowerShell.
|
3. Activate it using `source .venv/bin/activate` on Linux, `.venv\Scripts\activate.bat` in CMD or `.venv\Scripts\Activate.ps1` in PowerShell.
|
||||||
|
|
||||||
4. Install project's dependencies:
|
4. Install project's dependencies:
|
||||||
|
|
||||||
@@ -47,7 +47,8 @@ First you need to have a Python interpreter, MongoDB and optionally git. You can
|
|||||||
1. Copy file `config_example.json` to `config.json`
|
1. Copy file `config_example.json` to `config.json`
|
||||||
2. Open `config.json` using your favorite text editor. For example `nano config.json`
|
2. Open `config.json` using your favorite text editor. For example `nano config.json`
|
||||||
3. Change `"database"` keys to match your MongoDB setup
|
3. Change `"database"` keys to match your MongoDB setup
|
||||||
4. Change `"external_address"` to the ip/http address you may get in responses. By default it's `"localhost"`. This is extremely useful when running behind reverse-proxy.
|
4. Set the key `"secret"` to your JWT secret. You can type in anything, but long secrets are recommended. You can also set environment variable `PHOTOSAPI_SECRET` as an alternative
|
||||||
|
5. Change `"external_address"` to the ip/http address you may get in responses. By default it's `"localhost"`. This is extremely useful when running behind reverse-proxy.
|
||||||
|
|
||||||
After configuring everything listed above your API will be able to boot, however further configuration can be done. You can read about it in [repository's wiki](https://git.end-play.xyz/profitroll/PhotosAPI/wiki/Configuration). There's no need to focus on that now, it makes more sense to configure it afterwards.
|
After configuring everything listed above your API will be able to boot, however further configuration can be done. You can read about it in [repository's wiki](https://git.end-play.xyz/profitroll/PhotosAPI/wiki/Configuration). There's no need to focus on that now, it makes more sense to configure it afterwards.
|
||||||
|
|
||||||
@@ -58,6 +59,19 @@ First you need to have a Python interpreter, MongoDB and optionally git. You can
|
|||||||
|
|
||||||
Learn more about available uvicorn arguments using `uvicorn --help`
|
Learn more about available uvicorn arguments using `uvicorn --help`
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
When a new version comes out, sometimes you want to upgrade your instance right away. Here's a checklist what to do:
|
||||||
|
|
||||||
|
1. Carefully read the patch notes of the version you want to update to and all the versions that came out between the release of your version and the one you want to upgrade to.
|
||||||
|
Breaking changes will be marked so and config updates will also be described in the patch notes
|
||||||
|
2. Make a backup of your currently working instance. This includes both the PhotosAPI and the database
|
||||||
|
3. Download the latest version using git (`git pull` if you cloned the repo in the past) or from the releases
|
||||||
|
4. Reconfigure the config if needed and apply the changes from the patch notes
|
||||||
|
5. Upgrade the dependencies in your virtual environment using `pip install -r requirements.txt`
|
||||||
|
6. Start the migration using `python photos_api.py --migrate` from your virtual environment
|
||||||
|
7. Test if everything works and troubleshoot/rollback if not
|
||||||
|
|
||||||
## Using as a service
|
## Using as a service
|
||||||
|
|
||||||
It's a good practice to use your API as a systemd service on Linux. Here's a quick overview how that can be done.
|
It's a good practice to use your API as a systemd service on Linux. Here's a quick overview how that can be done.
|
||||||
@@ -83,7 +97,7 @@ It's a good practice to use your API as a systemd service on Linux. Here's a qui
|
|||||||
[Service]
|
[Service]
|
||||||
Restart=always
|
Restart=always
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/bin/bash -c 'source venv/bin/activate && venv/bin/uvicorn photos_api:app --port 8054'
|
ExecStart=/bin/bash -c 'source .venv/bin/activate && .venv/bin/uvicorn photos_api:app --port 8054'
|
||||||
WorkingDirectory=/opt/PhotosAPI
|
WorkingDirectory=/opt/PhotosAPI
|
||||||
User=photosapi
|
User=photosapi
|
||||||
Group=photosapi
|
Group=photosapi
|
||||||
|
@@ -286,3 +286,23 @@ class UserCredentialsInvalid(HTTPException):
|
|||||||
status_code=401,
|
status_code=401,
|
||||||
detail=self.openapi["content"]["application/json"]["example"]["detail"],
|
detail=self.openapi["content"]["application/json"]["example"]["detail"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserMediaQuotaReached(HTTPException):
|
||||||
|
"""Raises HTTP 403 if user's quota has been reached."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.openapi = {
|
||||||
|
"description": "Media Quota Reached",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"detail": "Media quota has been reached, media upload impossible."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
super().__init__(
|
||||||
|
status_code=403,
|
||||||
|
detail=self.openapi["content"]["application/json"]["example"]["detail"],
|
||||||
|
)
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
"user": null,
|
"user": null,
|
||||||
"password": null
|
"password": null
|
||||||
},
|
},
|
||||||
|
"secret": "",
|
||||||
"messages": {
|
"messages": {
|
||||||
"email_confirmed": "Email confirmed. You can now log in."
|
"email_confirmed": "Email confirmed. You can now log in."
|
||||||
},
|
},
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
"media_token_valid_hours": 12,
|
"media_token_valid_hours": 12,
|
||||||
"registration_enabled": true,
|
"registration_enabled": true,
|
||||||
"registration_requires_confirmation": false,
|
"registration_requires_confirmation": false,
|
||||||
|
"default_user_quota": 10000,
|
||||||
"mailer": {
|
"mailer": {
|
||||||
"smtp": {
|
"smtp": {
|
||||||
"host": "",
|
"host": "",
|
||||||
|
@@ -3,6 +3,7 @@ from fastapi.responses import UJSONResponse
|
|||||||
from starlette.status import (
|
from starlette.status import (
|
||||||
HTTP_400_BAD_REQUEST,
|
HTTP_400_BAD_REQUEST,
|
||||||
HTTP_401_UNAUTHORIZED,
|
HTTP_401_UNAUTHORIZED,
|
||||||
|
HTTP_403_FORBIDDEN,
|
||||||
HTTP_404_NOT_FOUND,
|
HTTP_404_NOT_FOUND,
|
||||||
HTTP_406_NOT_ACCEPTABLE,
|
HTTP_406_NOT_ACCEPTABLE,
|
||||||
HTTP_409_CONFLICT,
|
HTTP_409_CONFLICT,
|
||||||
@@ -10,19 +11,21 @@ from starlette.status import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from classes.exceptions import (
|
from classes.exceptions import (
|
||||||
AlbumNotFoundError,
|
AccessTokenInvalidError,
|
||||||
AlbumAlreadyExistsError,
|
AlbumAlreadyExistsError,
|
||||||
AlbumIncorrectError,
|
AlbumIncorrectError,
|
||||||
|
AlbumNotFoundError,
|
||||||
PhotoNotFoundError,
|
PhotoNotFoundError,
|
||||||
PhotoSearchQueryEmptyError,
|
PhotoSearchQueryEmptyError,
|
||||||
VideoNotFoundError,
|
SearchLimitInvalidError,
|
||||||
VideoSearchQueryEmptyError,
|
|
||||||
SearchPageInvalidError,
|
SearchPageInvalidError,
|
||||||
SearchTokenInvalidError,
|
SearchTokenInvalidError,
|
||||||
AccessTokenInvalidError,
|
|
||||||
UserEmailCodeInvalid,
|
|
||||||
UserAlreadyExists,
|
UserAlreadyExists,
|
||||||
UserCredentialsInvalid,
|
UserCredentialsInvalid,
|
||||||
|
UserEmailCodeInvalid,
|
||||||
|
UserMediaQuotaReached,
|
||||||
|
VideoNotFoundError,
|
||||||
|
VideoSearchQueryEmptyError,
|
||||||
)
|
)
|
||||||
from modules.app import app
|
from modules.app import app
|
||||||
|
|
||||||
@@ -93,12 +96,24 @@ async def video_search_query_empty_exception_handler(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(SearchLimitInvalidError)
|
||||||
|
async def search_limit_invalid_exception_handler(
|
||||||
|
request: Request, exc: SearchLimitInvalidError
|
||||||
|
):
|
||||||
|
return UJSONResponse(
|
||||||
|
status_code=HTTP_400_BAD_REQUEST,
|
||||||
|
content={
|
||||||
|
"detail": "Parameter 'limit' must be greater or equal to 1."
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(SearchPageInvalidError)
|
@app.exception_handler(SearchPageInvalidError)
|
||||||
async def search_page_invalid_exception_handler(
|
async def search_page_invalid_exception_handler(
|
||||||
request: Request, exc: SearchPageInvalidError
|
request: Request, exc: SearchPageInvalidError
|
||||||
):
|
):
|
||||||
return UJSONResponse(
|
return UJSONResponse(
|
||||||
status_code=HTTP_400_BAD_REQUEST,
|
status_code=HTTP_401_UNAUTHORIZED,
|
||||||
content={
|
content={
|
||||||
"detail": "Parameters 'page' and 'page_size' must be greater or equal to 1."
|
"detail": "Parameters 'page' and 'page_size' must be greater or equal to 1."
|
||||||
},
|
},
|
||||||
@@ -112,7 +127,7 @@ async def search_token_invalid_exception_handler(
|
|||||||
return UJSONResponse(
|
return UJSONResponse(
|
||||||
status_code=HTTP_401_UNAUTHORIZED,
|
status_code=HTTP_401_UNAUTHORIZED,
|
||||||
content={
|
content={
|
||||||
"detail": "Parameters 'page' and 'page_size' must be greater or equal to 1."
|
"detail": "Invalid search token."
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,3 +170,13 @@ async def user_credentials_invalid_exception_handler(
|
|||||||
status_code=HTTP_401_UNAUTHORIZED,
|
status_code=HTTP_401_UNAUTHORIZED,
|
||||||
content={"detail": "Invalid credentials."},
|
content={"detail": "Invalid credentials."},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(UserMediaQuotaReached)
|
||||||
|
async def user_media_quota_reached_exception_handler(
|
||||||
|
request: Request, exc: UserMediaQuotaReached
|
||||||
|
):
|
||||||
|
return UJSONResponse(
|
||||||
|
status_code=HTTP_403_FORBIDDEN,
|
||||||
|
content={"detail": "Media quota has been reached, media upload impossible."},
|
||||||
|
)
|
||||||
|
@@ -30,6 +30,7 @@ from classes.exceptions import (
|
|||||||
SearchLimitInvalidError,
|
SearchLimitInvalidError,
|
||||||
SearchPageInvalidError,
|
SearchPageInvalidError,
|
||||||
SearchTokenInvalidError,
|
SearchTokenInvalidError,
|
||||||
|
UserMediaQuotaReached,
|
||||||
)
|
)
|
||||||
from classes.models import (
|
from classes.models import (
|
||||||
Photo,
|
Photo,
|
||||||
@@ -38,7 +39,7 @@ from classes.models import (
|
|||||||
SearchResultsPhoto,
|
SearchResultsPhoto,
|
||||||
)
|
)
|
||||||
from modules.app import app
|
from modules.app import app
|
||||||
from modules.database import col_albums, col_photos, col_tokens
|
from modules.database import col_albums, col_photos, col_tokens, col_videos
|
||||||
from modules.exif_reader import extract_location
|
from modules.exif_reader import extract_location
|
||||||
from modules.hasher import get_duplicates, get_phash
|
from modules.hasher import get_duplicates, get_phash
|
||||||
from modules.scheduler import scheduler
|
from modules.scheduler import scheduler
|
||||||
@@ -91,6 +92,7 @@ async def compress_image(image_path: str):
|
|||||||
|
|
||||||
|
|
||||||
photo_post_responses = {
|
photo_post_responses = {
|
||||||
|
403: UserMediaQuotaReached().openapi,
|
||||||
404: AlbumNameNotFoundError("name").openapi,
|
404: AlbumNameNotFoundError("name").openapi,
|
||||||
409: {
|
409: {
|
||||||
"description": "Image Duplicates Found",
|
"description": "Image Duplicates Found",
|
||||||
@@ -125,6 +127,13 @@ async def photo_upload(
|
|||||||
if (await col_albums.find_one({"user": current_user.user, "name": album})) is None:
|
if (await col_albums.find_one({"user": current_user.user, "name": album})) is None:
|
||||||
raise AlbumNameNotFoundError(album)
|
raise AlbumNameNotFoundError(album)
|
||||||
|
|
||||||
|
user_media_count = (
|
||||||
|
await col_photos.count_documents({"user": current_user.user})
|
||||||
|
) + (await col_videos.count_documents({"user": current_user.user}))
|
||||||
|
|
||||||
|
if user_media_count >= current_user.quota and not current_user.quota == -1: # type: ignore
|
||||||
|
raise UserMediaQuotaReached()
|
||||||
|
|
||||||
makedirs(Path(f"data/users/{current_user.user}/albums/{album}"), exist_ok=True)
|
makedirs(Path(f"data/users/{current_user.user}/albums/{album}"), exist_ok=True)
|
||||||
|
|
||||||
filename = file.filename
|
filename = file.filename
|
||||||
|
@@ -109,6 +109,7 @@ if configGet("registration_enabled") is True:
|
|||||||
{
|
{
|
||||||
"user": user,
|
"user": user,
|
||||||
"email": email,
|
"email": email,
|
||||||
|
"quota": None,
|
||||||
"hash": get_password_hash(password),
|
"hash": get_password_hash(password),
|
||||||
"disabled": configGet("registration_requires_confirmation"),
|
"disabled": configGet("registration_requires_confirmation"),
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ from classes.exceptions import (
|
|||||||
SearchLimitInvalidError,
|
SearchLimitInvalidError,
|
||||||
SearchPageInvalidError,
|
SearchPageInvalidError,
|
||||||
SearchTokenInvalidError,
|
SearchTokenInvalidError,
|
||||||
|
UserMediaQuotaReached,
|
||||||
VideoNotFoundError,
|
VideoNotFoundError,
|
||||||
VideoSearchQueryEmptyError,
|
VideoSearchQueryEmptyError,
|
||||||
)
|
)
|
||||||
@@ -31,10 +32,13 @@ from classes.models import (
|
|||||||
VideoPublic,
|
VideoPublic,
|
||||||
)
|
)
|
||||||
from modules.app import app
|
from modules.app import app
|
||||||
from modules.database import col_albums, col_tokens, col_videos
|
from modules.database import col_albums, col_photos, col_tokens, col_videos
|
||||||
from modules.security import User, get_current_active_user
|
from modules.security import User, get_current_active_user
|
||||||
|
|
||||||
video_post_responses = {404: AlbumNameNotFoundError("name").openapi}
|
video_post_responses = {
|
||||||
|
403: UserMediaQuotaReached().openapi,
|
||||||
|
404: AlbumNameNotFoundError("name").openapi,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
@@ -53,6 +57,13 @@ async def video_upload(
|
|||||||
if (await col_albums.find_one({"user": current_user.user, "name": album})) is None:
|
if (await col_albums.find_one({"user": current_user.user, "name": album})) is None:
|
||||||
raise AlbumNameNotFoundError(album)
|
raise AlbumNameNotFoundError(album)
|
||||||
|
|
||||||
|
user_media_count = (
|
||||||
|
await col_videos.count_documents({"user": current_user.user})
|
||||||
|
) + (await col_photos.count_documents({"user": current_user.user}))
|
||||||
|
|
||||||
|
if user_media_count >= current_user.quota and not current_user.quota == -1: # type: ignore
|
||||||
|
raise UserMediaQuotaReached()
|
||||||
|
|
||||||
makedirs(Path(f"data/users/{current_user.user}/albums/{album}"), exist_ok=True)
|
makedirs(Path(f"data/users/{current_user.user}/albums/{album}"), exist_ok=True)
|
||||||
|
|
||||||
filename = file.filename
|
filename = file.filename
|
||||||
|
9
migrations/202311251700.py
Normal file
9
migrations/202311251700.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from mongodb_migrations.base import BaseMigration
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(BaseMigration):
|
||||||
|
def upgrade(self):
|
||||||
|
self.db.users.update_many({}, {"$set": {"quota": None}})
|
||||||
|
|
||||||
|
def downgrade(self):
|
||||||
|
self.db.test_collection.update_many({}, {"$unset": "quota"})
|
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||||
|
|
||||||
app = FastAPI(title="END PLAY Photos", docs_url=None, redoc_url=None, version="0.5")
|
app = FastAPI(title="END PLAY Photos", docs_url=None, redoc_url=None, version="0.6")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/docs", include_in_schema=False)
|
@app.get("/docs", include_in_schema=False)
|
||||||
|
23
modules/migrator.py
Normal file
23
modules/migrator.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from mongodb_migrations.cli import MigrationManager
|
||||||
|
from mongodb_migrations.config import Configuration
|
||||||
|
|
||||||
|
from modules.utils import configGet
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_database() -> None:
|
||||||
|
"""Apply migrations from folder `migrations/` to the database"""
|
||||||
|
db_config: Mapping[str, Any] = configGet("database")
|
||||||
|
|
||||||
|
manager_config = Configuration(
|
||||||
|
{
|
||||||
|
"mongo_host": db_config["host"],
|
||||||
|
"mongo_port": db_config["port"],
|
||||||
|
"mongo_database": db_config["name"],
|
||||||
|
"mongo_username": db_config["user"],
|
||||||
|
"mongo_password": db_config["password"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
manager = MigrationManager(manager_config)
|
||||||
|
manager.run()
|
@@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from os import getenv
|
||||||
from typing import List, Union
|
from typing import List, Union
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Security, status
|
from fastapi import Depends, HTTPException, Security, status
|
||||||
@@ -8,9 +9,26 @@ from passlib.context import CryptContext
|
|||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
from modules.database import col_users
|
from modules.database import col_users
|
||||||
|
from modules.utils import configGet
|
||||||
|
|
||||||
|
try:
|
||||||
|
configGet("secret")
|
||||||
|
except KeyError as exc:
|
||||||
|
raise KeyError(
|
||||||
|
"PhotosAPI secret is not set. Secret key handling has changed in PhotosAPI 0.6.0, so you need to add the config key 'secret' to your config file."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
if configGet("secret") == "" and getenv("PHOTOSAPI_SECRET") is None:
|
||||||
|
raise KeyError(
|
||||||
|
"PhotosAPI secret is not set. Set the config key 'secret' or provide the environment variable 'PHOTOSAPI_SECRET' containing a secret string."
|
||||||
|
)
|
||||||
|
|
||||||
|
SECRET_KEY = (
|
||||||
|
getenv("PHOTOSAPI_SECRET")
|
||||||
|
if getenv("PHOTOSAPI_SECRET") is not None
|
||||||
|
else configGet("secret")
|
||||||
|
)
|
||||||
|
|
||||||
with open("secret_key", "r", encoding="utf-8") as f:
|
|
||||||
SECRET_KEY = f.read()
|
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_DAYS = 180
|
ACCESS_TOKEN_EXPIRE_DAYS = 180
|
||||||
|
|
||||||
@@ -28,6 +46,7 @@ class TokenData(BaseModel):
|
|||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
user: str
|
user: str
|
||||||
email: Union[str, None] = None
|
email: Union[str, None] = None
|
||||||
|
quota: Union[int, None] = None
|
||||||
disabled: Union[bool, None] = None
|
disabled: Union[bool, None] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -71,6 +90,9 @@ async def get_user(user: str) -> UserInDB:
|
|||||||
return UserInDB(
|
return UserInDB(
|
||||||
user=found_user["user"],
|
user=found_user["user"],
|
||||||
email=found_user["email"],
|
email=found_user["email"],
|
||||||
|
quota=found_user["quota"]
|
||||||
|
if found_user["quota"] is not None
|
||||||
|
else configGet("default_user_quota"),
|
||||||
disabled=found_user["disabled"],
|
disabled=found_user["disabled"],
|
||||||
hash=found_user["hash"],
|
hash=found_user["hash"],
|
||||||
)
|
)
|
||||||
@@ -87,13 +109,16 @@ def create_access_token(
|
|||||||
data: dict, expires_delta: Union[timedelta, None] = None
|
data: dict, expires_delta: Union[timedelta, None] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
to_encode = data.copy()
|
to_encode = data.copy()
|
||||||
|
|
||||||
if expires_delta:
|
if expires_delta:
|
||||||
expire = datetime.now(tz=timezone.utc) + expires_delta
|
expire = datetime.now(tz=timezone.utc) + expires_delta
|
||||||
else:
|
else:
|
||||||
expire = datetime.now(tz=timezone.utc) + timedelta(
|
expire = datetime.now(tz=timezone.utc) + timedelta(
|
||||||
days=ACCESS_TOKEN_EXPIRE_DAYS
|
days=ACCESS_TOKEN_EXPIRE_DAYS
|
||||||
)
|
)
|
||||||
|
|
||||||
to_encode["exp"] = expire
|
to_encode["exp"] = expire
|
||||||
|
|
||||||
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
|
||||||
@@ -114,8 +139,10 @@ async def get_current_user(
|
|||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
user: str = payload.get("sub")
|
user: str = payload.get("sub")
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
token_scopes = payload.get("scopes", [])
|
token_scopes = payload.get("scopes", [])
|
||||||
token_data = TokenData(scopes=token_scopes, user=user)
|
token_data = TokenData(scopes=token_scopes, user=user)
|
||||||
except (JWTError, ValidationError) as exc:
|
except (JWTError, ValidationError) as exc:
|
||||||
@@ -133,6 +160,7 @@ async def get_current_user(
|
|||||||
detail="Not enough permissions",
|
detail="Not enough permissions",
|
||||||
headers={"WWW-Authenticate": authenticate_value},
|
headers={"WWW-Authenticate": authenticate_value},
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_record
|
return user_record
|
||||||
|
|
||||||
|
|
||||||
@@ -141,4 +169,5 @@ async def get_current_active_user(
|
|||||||
):
|
):
|
||||||
if current_user.disabled:
|
if current_user.disabled:
|
||||||
raise HTTPException(status_code=400, detail="Inactive user")
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
|
|
||||||
return current_user
|
return current_user
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from argparse import ArgumentParser
|
||||||
from os import makedirs
|
from os import makedirs
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@ from fastapi.responses import FileResponse
|
|||||||
|
|
||||||
from modules.app import app
|
from modules.app import app
|
||||||
from modules.extensions_loader import dynamic_import_from_src
|
from modules.extensions_loader import dynamic_import_from_src
|
||||||
|
from modules.migrator import migrate_database
|
||||||
from modules.scheduler import scheduler
|
from modules.scheduler import scheduler
|
||||||
|
|
||||||
makedirs(Path("data/users"), exist_ok=True)
|
makedirs(Path("data/users"), exist_ok=True)
|
||||||
@@ -27,3 +29,15 @@ dynamic_import_from_src("extensions", star_import=True)
|
|||||||
# =================================================================================
|
# =================================================================================
|
||||||
|
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
|
parser = ArgumentParser(
|
||||||
|
prog="PhotosAPI",
|
||||||
|
description="Small and simple API server for saving photos and videos.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("--migrate", action="store_true")
|
||||||
|
|
||||||
|
args, unknown = parser.parse_known_args()
|
||||||
|
|
||||||
|
if args.migrate:
|
||||||
|
migrate_database()
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
aiofiles==23.2.1
|
aiofiles==24.1.0
|
||||||
apscheduler~=3.10.1
|
apscheduler~=3.11.0
|
||||||
exif==1.6.0
|
async_pymongo==0.1.11
|
||||||
fastapi[all]==0.103.0
|
exif==1.6.1
|
||||||
opencv-python~=4.8.0.74
|
fastapi[all]==0.115.6
|
||||||
|
mongodb-migrations==1.3.1
|
||||||
|
opencv-python~=4.10.0.82
|
||||||
passlib~=1.7.4
|
passlib~=1.7.4
|
||||||
pymongo>=4.3.3
|
pymongo>=4.3.3
|
||||||
python-jose[cryptography]~=3.3.0
|
python-jose[cryptography]~=3.3.0
|
||||||
python-magic~=0.4.27
|
python-magic~=0.4.27
|
||||||
scipy~=1.11.0
|
scipy~=1.13.0,<1.14.0
|
||||||
ujson~=5.8.0
|
ujson~=5.10.0
|
||||||
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
|
|
||||||
async_pymongo==0.1.4
|
|
Reference in New Issue
Block a user