From 31d42f3ce780b3a5e5e1961201c1aa5061e69729 Mon Sep 17 00:00:00 2001 From: Profitroll <47523801+profitrollgame@users.noreply.github.com> Date: Sat, 7 Jan 2023 21:48:43 +0100 Subject: [PATCH] Email confirmations added --- config_example.json | 26 +++++++++++++- extensions/security.py | 80 +++++++++++++++++++++++++++++++----------- modules/database.py | 3 +- modules/mailer.py | 39 ++++++++++++++++++++ 4 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 modules/mailer.py diff --git a/config_example.json b/config_example.json index cf3d516..f59c4dc 100644 --- a/config_example.json +++ b/config_example.json @@ -11,6 +11,30 @@ "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." + "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." + }, + "external_address": "localhost", + "registration_enabled": true, + "registration_requires_confirmation": true, + "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}" + } + } } } \ No newline at end of file diff --git a/extensions/security.py b/extensions/security.py index 3ca9e0a..295b678 100644 --- a/extensions/security.py +++ b/extensions/security.py @@ -1,9 +1,16 @@ -from datetime import timedelta -from modules.database import col_users +from datetime import datetime, timedelta +from modules.database import col_users, col_albums, col_photos, col_emails, col_videos, col_emails, col_tokens from modules.app import app +from modules.utils import configGet, logWrite +from modules.scheduler import scheduler +from modules.mailer import mail_sender -from fastapi import Depends, HTTPException, Response -from starlette.status import HTTP_204_NO_CONTENT +from uuid import uuid1 +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from fastapi import Depends, HTTPException +from fastapi.responses import Response, UJSONResponse +from starlette.status import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_406_NOT_ACCEPTABLE from fastapi.security import ( OAuth2PasswordRequestForm, ) @@ -15,15 +22,30 @@ from modules.security import ( authenticate_user, create_access_token, get_current_active_user, - get_password_hash + get_password_hash, + get_user, + verify_password ) +async def send_confirmation(user: str, email: str): + confirmation_code = str(uuid1()) + try: + mail_sender.sendmail( + from_addr=configGet("sender", "mailer", "smtp"), + to_addrs=email, + msg=f'From: {configGet("sender", "mailer", "smtp")}\nSubject: Email confirmation\n\n'+configGet("message", "mailer", "messages", "registration_confirmation").format(configGet("external_address")+f"/users/{user}/confirm?code={confirmation_code}") + ) + col_emails.insert_one( {"user": user, "email": email, "used": False, "code": confirmation_code} ) + except Exception as exp: + logWrite(f"Could not send confirmation email to '{email}' due to: {exp}") + + @app.post("/token", response_model=Token) async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): user = authenticate_user(form_data.username, form_data.password) if not user: - raise HTTPException(status_code=400, detail="Incorrect user or password") + raise HTTPException(status_code=400, detail=configGet("credentials_invalid", "messages")) access_token_expires = timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS) access_token = create_access_token( data={"sub": user.user, "scopes": form_data.scopes}, @@ -37,19 +59,37 @@ async def read_users_me(current_user: User = Depends(get_current_active_user)): return current_user -@app.post("/users", response_class=Response) -async def create_users(user: str, email: str, password: str): - col_users.insert_one( {"user": user, "email": email, "hash": get_password_hash(password), "disabled": True} ) - return Response(status_code=HTTP_204_NO_CONTENT) +if configGet("registration_requires_confirmation") is True: + @app.get("/users/{user}/confirm") + @app.patch("/users/{user}/confirm") + async def confirm_users(user: str, code: str): + confirm_record = col_emails.find_one( {"user": user, "code": code, "used": False} ) + if confirm_record is None: + return HTTPException(HTTP_400_BAD_REQUEST, detail=configGet("email_code_invalid", "messages")) + col_emails.find_one_and_update( {"_id": confirm_record["_id"]}, {"$set": {"used": True}} ) + col_users.find_one_and_update( {"user": confirm_record["user"]}, {"$set": {"disabled": False}} ) + return UJSONResponse( {"detail": configGet("email_confirmed", "messages")} ) + +if configGet("registration_enabled") is True: + @app.post("/users") + async def create_users(user: str, email: str, password: str): + if col_users.find_one( {"user": user} ) is not None: + return HTTPException(HTTP_406_NOT_ACCEPTABLE, detail=configGet("user_already_exists", "messages")) + col_users.insert_one( {"user": user, "email": email, "hash": get_password_hash(password), "disabled": configGet("registration_requires_confirmation")} ) + if configGet("registration_requires_confirmation") is True: + scheduler.add_job( send_confirmation, trigger="date", run_date=datetime.now()+timedelta(seconds=1), kwargs={"user": user, "email": email} ) + return Response(status_code=HTTP_204_NO_CONTENT) -# @app.get("/users/me/items/") -# async def read_own_items( -# current_user: User = Security(get_current_active_user, scopes=["items"]) -# ): -# return [{"item_id": "Foo", "owner": current_user.user}] - - -# @app.get("/status/") -# async def read_system_status(current_user: User = Depends(get_current_user)): -# return {"status": "ok"} \ No newline at end of file +@app.delete("/users/me/") +async def delete_users(password: str, current_user: User = Depends(get_current_active_user)): + user = get_user(current_user.user) + if not user: + return False + if not verify_password(password, user.hash): + return HTTPException(HTTP_400_BAD_REQUEST, detail=configGet("credentials_invalid", "messages")) + col_users.delete_many( {"user": current_user.user} ) + col_emails.delete_many( {"user": current_user.user} ) + col_photos.delete_many( {"user": current_user.user} ) + col_videos.delete_many( {"user": current_user.user} ) + col_albums.delete_many( {"user": current_user.user} ) \ No newline at end of file diff --git a/modules/database.py b/modules/database.py index b36d6f7..aefc193 100644 --- a/modules/database.py +++ b/modules/database.py @@ -24,7 +24,7 @@ db = db_client.get_database(name=db_config["name"]) collections = db.list_collection_names() -for collection in ["users", "albums", "photos", "videos", "tokens"]: +for collection in ["users", "albums", "photos", "videos", "tokens", "emails"]: if not collection in collections: db.create_collection(collection) @@ -33,5 +33,6 @@ col_albums = db.get_collection("albums") col_photos = db.get_collection("photos") col_videos = db.get_collection("videos") col_tokens = db.get_collection("tokens") +col_emails = db.get_collection("emails") col_photos.create_index([("location", GEOSPHERE)]) \ No newline at end of file diff --git a/modules/mailer.py b/modules/mailer.py new file mode 100644 index 0000000..a0d9428 --- /dev/null +++ b/modules/mailer.py @@ -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}") \ No newline at end of file