update emails and verifing

This commit is contained in:
ben
2026-08-18 11:32:01 +02:00
parent 5f77a4d995
commit 953c371e48
3 changed files with 357 additions and 14 deletions
-12
View File
@@ -5,9 +5,6 @@ DB_PATH = Path(__file__).parent.parent / "database" / "calendar.db"
def get_connection(): def get_connection():
"""
Maakt een verbinding met de SQLite database.
"""
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA foreign_keys = ON")
@@ -18,9 +15,6 @@ def get_connection():
def execute(query, params=()): def execute(query, params=()):
"""
Voor INSERT, UPDATE, DELETE.
"""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query, params) cursor.execute(query, params)
@@ -30,9 +24,6 @@ def execute(query, params=()):
def fetch_one(query, params=()): def fetch_one(query, params=()):
"""
Haalt één resultaat op.
"""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query, params) cursor.execute(query, params)
@@ -52,9 +43,6 @@ def fetch_all(query, params=()):
def test_connection(): def test_connection():
"""
Test of de database bereikbaar is.
"""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
+272
View File
@@ -0,0 +1,272 @@
import hashlib
import secrets
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path
import os
import smtplib
from email.message import EmailMessage
from dotenv import load_dotenv
load_dotenv()
DB_NAME = Path(__file__).parent / "database" / "calendar.db"
def get_connection():
conn = sqlite3.connect(DB_NAME)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON;")
return conn
def hash_token(token: str) -> str:
return hashlib.sha256(
token.encode("utf-8")
).hexdigest()
def update_email(user_id: int, new_email: str):
new_email = new_email.strip().lower()
conn = get_connection()
cur = conn.cursor()
try:
cur.execute(
"""
SELECT id
FROM users
WHERE id = ?
""",
(user_id,),
)
if cur.fetchone() is None:
return {
"status": "failed",
"reason": "user_not_found",
}
cur.execute(
"""
SELECT id
FROM users
WHERE email = ?
AND id != ?
""",
(new_email, user_id),
)
if cur.fetchone() is not None:
return {
"status": "failed",
"reason": "email_already_in_use",
}
cur.execute(
"""
UPDATE users
SET email = ?,
email_verified = 0
WHERE id = ?
""",
(new_email, user_id),
)
cur.execute(
"""
DELETE FROM email_verifications
WHERE user_id = ?
""",
(user_id,),
)
conn.commit()
return {
"status": "updated",
"email": new_email,
"email_verified": False,
}
except sqlite3.IntegrityError:
conn.rollback()
return {
"status": "failed",
"reason": "email_already_in_use",
}
finally:
conn.close()
def create_verification(user_id: int):
token = secrets.token_urlsafe(32)
token_hash = hash_token(token)
expires_at = (
datetime.now(timezone.utc)
+ timedelta(hours=24)
).isoformat()
conn = get_connection()
cur = conn.cursor()
try:
cur.execute(
"""
DELETE FROM email_verifications
WHERE user_id = ?
""",
(user_id,),
)
cur.execute(
"""
INSERT INTO email_verifications (
user_id,
token_hash,
expires_at
)
VALUES (?, ?, ?)
""",
(
user_id,
token_hash,
expires_at,
),
)
conn.commit()
return token
finally:
conn.close()
def verify_email(token: str):
token_hash = hash_token(token)
conn = get_connection()
cur = conn.cursor()
try:
cur.execute(
"""
SELECT
id,
user_id,
expires_at
FROM email_verifications
WHERE token_hash = ?
""",
(token_hash,),
)
verification = cur.fetchone()
if verification is None:
return {
"status": "failed",
"reason": "invalid_token",
}
expires_at = datetime.fromisoformat(
verification["expires_at"]
)
if datetime.now(timezone.utc) >= expires_at:
cur.execute(
"""
DELETE FROM email_verifications
WHERE id = ?
""",
(verification["id"],),
)
conn.commit()
return {
"status": "failed",
"reason": "token_expired",
}
cur.execute(
"""
UPDATE users
SET email_verified = 1
WHERE id = ?
""",
(verification["user_id"],),
)
cur.execute(
"""
DELETE FROM email_verifications
WHERE id = ?
""",
(verification["id"],),
)
conn.commit()
return {
"status": "verified",
}
finally:
conn.close()
def send_verification_email(email: str, token: str):
smtp_host = os.getenv("SMTP_HOST")
smtp_port = int(os.getenv("SMTP_PORT", "587"))
smtp_username = os.getenv("SMTP_USERNAME")
smtp_password = os.getenv("SMTP_PASSWORD")
smtp_from = os.getenv("SMTP_FROM")
if not all([
smtp_host,
smtp_username,
smtp_password,
smtp_from,
]):
raise RuntimeError("SMTP configuration is incomplete")
verification_url = (
f"https://jouwdomein.nl/verify-email?token={token}"
)
message = EmailMessage()
message["From"] = smtp_from
message["To"] = email
message["Subject"] = "Verify your Calendar email address"
message.set_content(
f"""Hello,
You requested to use this email address for your Calendar account.
Verify your email address using this link:
{verification_url}
This link expires after 24 hours.
If you did not request this, you can ignore this email.
"""
)
with smtplib.SMTP(smtp_host, smtp_port) as smtp:
smtp.starttls()
smtp.login(
smtp_username,
smtp_password,
)
smtp.send_message(message)
+85 -2
View File
@@ -1,15 +1,24 @@
# GEEN SQL IN MAIN.PY, DIT IS DE API, HOU HET OVERZICHTELIJK!! # GEEN SQL IN MAIN.PY, DIT IS DE API/ERMISSIECHECK, HOU HET OVERZICHTELIJK!!
from datetime import date from datetime import date
from fastapi import FastAPI, Depends, HTTPException, Query from fastapi import FastAPI, Depends, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import PlainTextResponse from fastapi.responses import PlainTextResponse
from pydantic import BaseModel from pydantic import BaseModel, EmailStr
from typing import Literal from typing import Literal
import sqlite3 import sqlite3
from datetime import datetime, timezone
from fastapi import Query
from database import fetch_one, execute
from email_service import hash_token
from database import get_table_names from database import get_table_names
from email_service import (
update_email
)
from auth import ( from auth import (
create_user, create_user,
verify_user, verify_user,
@@ -59,6 +68,8 @@ from event_signups import (
get_event_responses, get_event_responses,
) )
class EmailUpdate(BaseModel):
email: EmailStr
class MemberRoleUpdate(BaseModel): class MemberRoleUpdate(BaseModel):
role: Literal["member", "admin"] role: Literal["member", "admin"]
@@ -976,6 +987,78 @@ def api_delete_event(
"status": "deleted", "status": "deleted",
} }
# email endpoints
@app.put(
"/me/email",
summary="Update email address",
description="Changes the authenticated user's email address.",
)
def api_update_email(
data: EmailUpdate,
user_id: int = Depends(get_current_user),
):
return update_email(
user_id,
data.email,
)
@app.get("/verify-email")
def verify_email(token: str = Query(...)):
token_hash = hash_token(token)
verification = fetch_one(
"""
SELECT id, user_id, expires_at
FROM email_verifications
WHERE token_hash = ?
""",
(token_hash,)
)
if not verification:
raise HTTPException(
status_code=400,
detail="Invalid verification token"
)
expires_at = datetime.fromisoformat(verification["expires_at"])
if expires_at < datetime.now(timezone.utc):
execute(
"""
DELETE FROM email_verifications
WHERE id = ?
""",
(verification["id"],)
)
raise HTTPException(
status_code=400,
detail="Verification token has expired"
)
execute(
"""
UPDATE users
SET email_verified = 1
WHERE id = ?
""",
(verification["user_id"],)
)
execute(
"""
DELETE FROM email_verifications
WHERE id = ?
""",
(verification["id"],)
)
return {
"status": "verified",
"message": "Email address successfully verified"
}
# easter egg # easter egg