Skip to content

Commit

Permalink
Account Deletion Endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnGrubba committed Jul 25, 2024
1 parent 981b95e commit f6159ba
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 9 deletions.
4 changes: 3 additions & 1 deletion config/configtemplate.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
"qr_endpoint": true
},
"allow_add_fields_on_signup": [],
"allow_add_fields_patch_user": []
"allow_add_fields_patch_user": [],
"allow_deletion": true,
"deletion_pending_minutes": 10080
},
"security": {
"allow_origins": [
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Make sure that all parameters are set correctly before starting the service.
| `account_features.2fa.qr_endpoint` | **Datatype:** Boolean <br> **Default:** `true` <br> Enable or disable QR Code Generation Endpoint for 2FA Login. This can be useful if you don't want to use any libraries on the client Side. |
| `account_features.allow_add_fields_on_signup` | **Datatype:** List <br> **Default:** `[]` <br> Allow those additional fields on signup. Leave empty if not sure. |
| `account_features.allow_add_fields_patch_user` | **Datatype:** List <br> **Default:** `[]` <br> Allow those additional fields to be set when modifying user. Leave empty if not sure. The entries here extend already set `account_features.allow_add_fields_on_signup` fields. |
| `account_features.allow_deletion` | **Datatype:** Boolean <br> **Default:** `true` <br> Allow the user to request an account deletion. |
| `account_features.deletion_pending_minutes` | **Datatype:** Integer <br> **Default:** `10080` <br> Minutes before the account gets deleted. Directly after requesting deletion, the User can't log in anymore, but the data will be persisted until this value passes by. Example Value is a Week. |

!!! Note "Additional Fields"
The `allow_add_fields_on_signup` makes it possible to add custom fields to the signup process. If you don't set the fields that are allowed here on signup, you can't update them later, except you also have them in `allow_add_fields_patch_user`.
Expand Down
4 changes: 4 additions & 0 deletions src/api/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ def password_check_hash(cls, password: SecretStr) -> str:
return hashed_pswd


class DeleteAccountRequest(BaseModel):
password: SecretStr


class ResetPasswordRequest(PasswordHashed):
old_password: SecretStr

Expand Down
35 changes: 32 additions & 3 deletions src/api/profile.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, Response
from tools.conf import AccountFeaturesConfig, SignupConfig
from api.model import ResetPasswordRequest, ConfirmEmailRequest
from api.model import ResetPasswordRequest, ConfirmEmailRequest, DeleteAccountRequest
import json
import bcrypt
from crud.user import change_pswd, update_public_user
from crud.user import change_pswd, update_public_user, schedule_delete_user
from api.dependencies.authenticated import (
get_pub_user_dep,
get_dangerous_user_dep,
get_user_dep,
)
from tools import send_email, all_ids, regenerate_ids, r
from tools import send_email, all_ids, regenerate_ids, r, SessionConfig

router = APIRouter(
prefix="/profile",
Expand Down Expand Up @@ -137,3 +137,32 @@ async def update_profile(update_data: dict, user: dict = Depends(get_user_dep)):
This endpoint is used to update the profile information of the user.
"""
return update_public_user(user["_id"], update_data)


@router.delete("", status_code=204)
async def delete_account(
password: DeleteAccountRequest,
response: Response,
user: dict = Depends(get_dangerous_user_dep),
):
"""
# Delete Account
## Description
This endpoint is used to request a deletion of the user's account.
This process can only be canceled by the administration of the system.
"""
if not AccountFeaturesConfig.allow_deletion:
raise HTTPException(status_code=403, detail="Account Deletion is disabled.")
# Check Password
if not bcrypt.checkpw(
password.password.get_secret_value().encode("utf-8"),
user["password"].encode("utf-8"),
):
raise HTTPException(detail="Invalid Password", status_code=401)
schedule_delete_user(user["_id"])
response.delete_cookie(
SessionConfig.auto_cookie_name,
samesite=SessionConfig.cookie_samesite,
secure=SessionConfig.cookie_secure,
)
20 changes: 19 additions & 1 deletion src/crud/sessions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import uuid
from tools import sessions_collection
from tools import sessions_collection, users_collection
import datetime
from tools.conf import SessionConfig
from fastapi import Request
Expand All @@ -19,6 +19,14 @@ def create_login_session(user_id: ObjectId, request: Request) -> str:
Returns:
str: Session Token
"""
# Check if User is already scheduled for deletion before creating a session
user = users_collection.find_one({"_id": ObjectId(user_id)})
if user.get("expiresAfter", None) is not None:
raise HTTPException(
status_code=403,
detail="User scheduled for deletion. Please Contact an Administrator for more information.",
)

u_agent = request.headers.get("User-Agent")
ua = parse(u_agent)
device_information = {
Expand Down Expand Up @@ -115,3 +123,13 @@ def get_session_by_id(session_id: str) -> dict:
except errors.InvalidId:
raise HTTPException(status_code=404, detail="Session not found.")
return sessions_collection.find_one({"_id": object_id})


def clear_sessions_for_user(user_id: ObjectId) -> None:
"""
Clear all sessions for a user.
Args:
user_id (str): User ID
"""
sessions_collection.delete_many({"user_id": user_id})
21 changes: 21 additions & 0 deletions src/crud/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pymongo
import bson
import datetime
from crud.sessions import clear_sessions_for_user


def link_google_account(user_id: str, google_uid: str) -> None:
Expand Down Expand Up @@ -251,3 +252,23 @@ def create_user(
if SignupConfig.enable_welcome_email:
background_tasks.add_task(send_email, "WelcomeEmail", data["email"], **data)
return session_token


def schedule_delete_user(user_id: str) -> None:
"""Schedule a User for Deletion
Args:
user_id (str): User ID
"""
users_collection.update_one(
{"_id": bson.ObjectId(user_id)},
{
"$set": {
"expiresAfter": datetime.datetime.now(datetime.UTC)
+ datetime.timedelta(
minutes=AccountFeaturesConfig.deletion_pending_minutes
)
}
},
)
clear_sessions_for_user(user_id)
10 changes: 7 additions & 3 deletions src/tools/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# Columns that should never leave EZAuth (maybe get more in the future)
default_signup_fields = {"username", "email", "password"}
insecure_cols = {"password": 0, "2fa_secret": 0, "google_uid": 0, "github_uid": 0}
not_updateable_cols_internal = ["email", "createdAt"]
not_updateable_cols_internal = ["email", "createdAt", "expireAt"]
# Columns that can leave EZAuth but should only be used internally can be defined in config


Expand Down Expand Up @@ -68,10 +68,14 @@ class AccountFeaturesConfig:
qr_code_endpoint_2fa: bool = config["account_features"]["2fa"]["qr_endpoint"]
allow_add_fields_on_signup: set[str] = set(
config["account_features"]["allow_add_fields_on_signup"]
)
) - set(not_updateable_cols_internal)
allow_add_fields_patch_user: set[str] = set(
config["account_features"]["allow_add_fields_patch_user"]
)
) - set(not_updateable_cols_internal)
allow_deletion: bool = config["account_features"]["allow_deletion"]
deletion_pending_minutes: int = config["account_features"][
"deletion_pending_minutes"
]


class SecurityConfig:
Expand Down
8 changes: 7 additions & 1 deletion src/tools/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,14 @@
pass
# Set TTL For Sessions
sessions_collection.create_index(
"createdAt", expireAfterSeconds=SessionConfig.session_expiry_seconds
"createdAt", expireAfterSeconds=SessionConfig.session_expiry_seconds, sparse=True
)
try:
users_collection.drop_index("expiresAfter_1")
except Exception:
pass
# Create TTL For Account Deletions
users_collection.create_index("expiresAfter", expireAfterSeconds=0, sparse=True)


def bson_to_json(data: bson.BSON) -> dict:
Expand Down

0 comments on commit f6159ba

Please sign in to comment.