Compare commits
16
Commits
3729d965f2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfbd561763 | ||
|
|
fcb6690af5 | ||
|
|
08e02dd556 | ||
|
|
a980812137 | ||
|
|
e61750f314 | ||
|
|
af1c147fd0 | ||
|
|
792ce850b6 | ||
|
|
fae25adb99 | ||
|
|
8fae1ffb3d | ||
|
|
bfab69cbb4 | ||
|
|
468c34ff92 | ||
|
|
8fe812e8e6 | ||
|
|
bf601846db | ||
|
|
1822dea7e3 | ||
|
|
63bfaf9619 | ||
|
|
bbb784c3f1 |
+27
-6
@@ -1,6 +1,27 @@
|
||||
.env
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
database/calendar.db
|
||||
database/calendar.db-journal
|
||||
.vscode
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
-1
@@ -102,8 +102,9 @@ def get_current_user(
|
||||
algorithms=[ALGORITHM]
|
||||
)
|
||||
|
||||
return payload["user_id"]
|
||||
print("DEBUG JWT payload:", payload)
|
||||
|
||||
return payload["user_id"]
|
||||
|
||||
def get_user(user_id):
|
||||
return fetch_one(
|
||||
|
||||
+216
-125
@@ -1,17 +1,19 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import smtplib
|
||||
import sqlite3
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.message import EmailMessage
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
DB_NAME = Path(__file__).parent.parent / "database" / "calendar.db"
|
||||
|
||||
|
||||
@@ -27,8 +29,198 @@ def hash_token(token: str) -> str:
|
||||
token.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
def send_email(
|
||||
email: str,
|
||||
subject: str,
|
||||
plain_text: str,
|
||||
html: 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")
|
||||
|
||||
def update_email(user_id: int, new_email: str):
|
||||
if not all([
|
||||
smtp_host,
|
||||
smtp_username,
|
||||
smtp_password,
|
||||
smtp_from,
|
||||
]):
|
||||
raise RuntimeError("SMTP configuration is incomplete")
|
||||
|
||||
message = EmailMessage()
|
||||
|
||||
message["From"] = smtp_from
|
||||
message["To"] = email
|
||||
message["Subject"] = subject
|
||||
|
||||
# Plain-text fallback
|
||||
message.set_content(plain_text)
|
||||
|
||||
# HTML version
|
||||
message.add_alternative(
|
||||
html,
|
||||
subtype="html",
|
||||
)
|
||||
|
||||
print(f"Sending email to {email}")
|
||||
|
||||
with smtplib.SMTP(
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
) as smtp:
|
||||
|
||||
print("SMTP connected")
|
||||
|
||||
smtp.starttls()
|
||||
|
||||
print("TLS started")
|
||||
|
||||
smtp.login(
|
||||
smtp_username,
|
||||
smtp_password,
|
||||
)
|
||||
|
||||
print("SMTP login successful")
|
||||
|
||||
smtp.send_message(message)
|
||||
|
||||
print("Email sent")
|
||||
|
||||
|
||||
def send_verification_email(
|
||||
email: str,
|
||||
token: str,
|
||||
):
|
||||
verification_url = (
|
||||
"https://ben.de-roo.org"
|
||||
f"/api/verify-email?token={token}"
|
||||
)
|
||||
|
||||
plain_text = 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.
|
||||
"""
|
||||
|
||||
html = f"""\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
You requested to use this email address
|
||||
for your Calendar account.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="{verification_url}">
|
||||
https://ben.de-roo.org/api/verify
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This link expires after 24 hours.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you did not request this,
|
||||
you can ignore this email.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
send_email(
|
||||
email=email,
|
||||
subject="Verify your Calendar email address",
|
||||
plain_text=plain_text,
|
||||
html=html,
|
||||
)
|
||||
|
||||
|
||||
def send_password_reset_email(
|
||||
email: str,
|
||||
token: str,
|
||||
):
|
||||
password_reset_url = (
|
||||
"https://ben.de-roo.org"
|
||||
f"/api/update-password_page?token={token}"
|
||||
)
|
||||
|
||||
plain_text = f"""Hello,
|
||||
|
||||
You requested to reset your Calendar password.
|
||||
|
||||
Reset your password using this link:
|
||||
|
||||
{password_reset_url}
|
||||
|
||||
This link expires after 1 hour.
|
||||
|
||||
If you did not request this, you can ignore this email.
|
||||
"""
|
||||
|
||||
html = f"""\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>
|
||||
You requested to reset your Calendar password.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="{password_reset_url}">
|
||||
https://ben.de-roo.org/api/update-password_page
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This link expires after 1 hour.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you did not request this,
|
||||
you can ignore this email.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
send_email(
|
||||
email=email,
|
||||
subject="Reset your password",
|
||||
plain_text=plain_text,
|
||||
html=html,
|
||||
)
|
||||
|
||||
def update_email(
|
||||
user_id: int,
|
||||
new_email: str,
|
||||
):
|
||||
new_email = new_email.strip().lower()
|
||||
|
||||
conn = get_connection()
|
||||
@@ -57,7 +249,10 @@ def update_email(user_id: int, new_email: str):
|
||||
WHERE email = ?
|
||||
AND id != ?
|
||||
""",
|
||||
(new_email, user_id),
|
||||
(
|
||||
new_email,
|
||||
user_id,
|
||||
),
|
||||
)
|
||||
|
||||
if cur.fetchone() is not None:
|
||||
@@ -73,7 +268,10 @@ def update_email(user_id: int, new_email: str):
|
||||
email_verified = 0
|
||||
WHERE id = ?
|
||||
""",
|
||||
(new_email, user_id),
|
||||
(
|
||||
new_email,
|
||||
user_id,
|
||||
),
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
@@ -104,7 +302,9 @@ def update_email(user_id: int, new_email: str):
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_verification(user_id: int):
|
||||
def create_verification(
|
||||
user_id: int,
|
||||
):
|
||||
token = secrets.token_urlsafe(32)
|
||||
token_hash = hash_token(token)
|
||||
|
||||
@@ -149,7 +349,9 @@ def create_verification(user_id: int):
|
||||
conn.close()
|
||||
|
||||
|
||||
def verify_email(token: str):
|
||||
def verify_email(
|
||||
token: str,
|
||||
):
|
||||
token_hash = hash_token(token)
|
||||
|
||||
conn = get_connection()
|
||||
@@ -181,6 +383,7 @@ def verify_email(token: str):
|
||||
)
|
||||
|
||||
if datetime.now(timezone.utc) >= expires_at:
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
DELETE FROM email_verifications
|
||||
@@ -222,121 +425,9 @@ def verify_email(token: str):
|
||||
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://ben.de-roo.org/api/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.
|
||||
"""
|
||||
)
|
||||
|
||||
print(f"Sending verification email to {email}")
|
||||
|
||||
with smtplib.SMTP(smtp_host, smtp_port) as smtp:
|
||||
print("SMTP connected")
|
||||
|
||||
smtp.starttls()
|
||||
print("TLS started")
|
||||
|
||||
smtp.login(
|
||||
smtp_username,
|
||||
smtp_password,
|
||||
)
|
||||
print("SMTP login successful")
|
||||
|
||||
smtp.send_message(message)
|
||||
print("Verification email sent")
|
||||
|
||||
def send_password_reset_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")
|
||||
|
||||
password_reset_url = (
|
||||
f"https://ben.de-roo.org/api/update-password?token={token}"
|
||||
)
|
||||
|
||||
message = EmailMessage()
|
||||
|
||||
message["From"] = smtp_from
|
||||
message["To"] = email
|
||||
message["Subject"] = "Reset your password"
|
||||
|
||||
message.set_content(
|
||||
f"""Hello,
|
||||
|
||||
You requested to use this email address for reseting your Calendar password.
|
||||
|
||||
Reset your password using this link:
|
||||
|
||||
{password_reset_url}
|
||||
|
||||
This link expires after 24 hours.
|
||||
|
||||
If you did not request this, you can ignore this email.
|
||||
"""
|
||||
)
|
||||
|
||||
print(f"Sending password reset email to {email}")
|
||||
|
||||
with smtplib.SMTP(smtp_host, smtp_port) as smtp:
|
||||
print("SMTP connected")
|
||||
|
||||
smtp.starttls()
|
||||
print("TLS started")
|
||||
|
||||
smtp.login(
|
||||
smtp_username,
|
||||
smtp_password,
|
||||
)
|
||||
print("SMTP login successful")
|
||||
|
||||
smtp.send_message(message)
|
||||
print("Password reset email sent")
|
||||
|
||||
def create_password_reset(user_id: int):
|
||||
def create_password_reset(
|
||||
user_id: int,
|
||||
):
|
||||
token = secrets.token_urlsafe(32)
|
||||
token_hash = hash_token(token)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from database import execute, fetch_one
|
||||
from database import execute, fetch_one, fetch_all
|
||||
|
||||
|
||||
def respond_to_event(event_id, user_id, status):
|
||||
|
||||
+705
-107
@@ -339,8 +339,13 @@ def api_update_member_role(
|
||||
data: MemberRoleUpdate,
|
||||
current_user: int = Depends(get_current_user),
|
||||
):
|
||||
current_role = get_role(room_id, current_user)
|
||||
target_role = get_role(room_id, user_id)
|
||||
role_levels = {
|
||||
"member": 1,
|
||||
"admin": 2,
|
||||
"owner": 3,
|
||||
}
|
||||
current_role = get_role(room_id, current_user) or 0
|
||||
target_role = get_role(room_id, user_id) or 0
|
||||
|
||||
if current_role is None:
|
||||
raise HTTPException(
|
||||
@@ -1077,57 +1082,333 @@ def api_update_email(
|
||||
@app.get(
|
||||
"/verify-email",
|
||||
response_class=HTMLResponse,
|
||||
summary="Verify email address",
|
||||
)
|
||||
def api_verify_email(token: str = Query(...)):
|
||||
def api_verify_email(
|
||||
token: str = Query(...),
|
||||
):
|
||||
result = verify_email(token)
|
||||
|
||||
if result["status"] == "failed":
|
||||
if result["reason"] == "token_expired":
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Verification expired</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Verification expired</h1>
|
||||
<p>This verification link has expired.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(
|
||||
content="""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Verification failed</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Verification failed</h1>
|
||||
<p>This verification link is invalid.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
<title>Verification expired</title>
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email verified</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>Email verified</h1>
|
||||
<p>Your email address has been successfully verified.</p>
|
||||
<p>You may now close this tab.</p>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
|
||||
background: #0b0d10;
|
||||
color: #e8eaed;
|
||||
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 40px;
|
||||
|
||||
background: #15181d;
|
||||
border: 1px solid #2a2f36;
|
||||
border-radius: 14px;
|
||||
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 24px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 50%;
|
||||
background: #2b2115;
|
||||
color: #f0a83b;
|
||||
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #9da4ae;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 28px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #272c33;
|
||||
|
||||
color: #6f7782;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="card">
|
||||
<div class="icon">!</div>
|
||||
|
||||
<h1>Verification expired</h1>
|
||||
|
||||
<p>
|
||||
This email verification link has expired.
|
||||
Please request a new verification email.
|
||||
</p>
|
||||
|
||||
<div class="footer">
|
||||
Calendar API
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=410,
|
||||
)
|
||||
|
||||
return HTMLResponse(
|
||||
content="""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Verification failed</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
|
||||
background: #0b0d10;
|
||||
color: #e8eaed;
|
||||
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 40px;
|
||||
|
||||
background: #15181d;
|
||||
border: 1px solid #2a2f36;
|
||||
border-radius: 14px;
|
||||
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 24px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 50%;
|
||||
background: #2a1719;
|
||||
color: #ef6461;
|
||||
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #9da4ae;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 28px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #272c33;
|
||||
|
||||
color: #6f7782;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="card">
|
||||
<div class="icon">×</div>
|
||||
|
||||
<h1>Verification failed</h1>
|
||||
|
||||
<p>
|
||||
This verification link is invalid or has already been used.
|
||||
</p>
|
||||
|
||||
<div class="footer">
|
||||
Calendar API
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
return HTMLResponse(
|
||||
content="""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Email verified</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
|
||||
background: #0b0d10;
|
||||
color: #e8eaed;
|
||||
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 40px;
|
||||
|
||||
background: #15181d;
|
||||
border: 1px solid #2a2f36;
|
||||
border-radius: 14px;
|
||||
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 24px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 50%;
|
||||
background: #14271d;
|
||||
color: #4ade80;
|
||||
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0;
|
||||
color: #9da4ae;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 28px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #272c33;
|
||||
|
||||
color: #6f7782;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="card">
|
||||
<div class="icon">✓</div>
|
||||
|
||||
<h1>Email verified</h1>
|
||||
|
||||
<p>
|
||||
Your email address has been successfully verified.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You may now close this tab.
|
||||
</p>
|
||||
|
||||
<div class="footer">
|
||||
Calendar API
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
)
|
||||
|
||||
@app.post(
|
||||
"/forgot-password",
|
||||
@@ -1142,47 +1423,194 @@ def api_forgot_password(
|
||||
@app.get(
|
||||
"/update-password_page",
|
||||
response_class=HTMLResponse,
|
||||
summary="Password reset page",
|
||||
)
|
||||
def update_password_page(token: str = Query(...)):
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reset password</title>
|
||||
</head>
|
||||
<body>
|
||||
def update_password_page(
|
||||
token: str = Query(...),
|
||||
):
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Reset password</title>
|
||||
|
||||
<style>
|
||||
* {{
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 24px;
|
||||
|
||||
background: #0b0d10;
|
||||
color: #e8eaed;
|
||||
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
}}
|
||||
|
||||
.card {{
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 40px;
|
||||
|
||||
background: #15181d;
|
||||
border: 1px solid #2a2f36;
|
||||
border-radius: 14px;
|
||||
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
|
||||
}}
|
||||
|
||||
h1 {{
|
||||
margin: 0 0 10px;
|
||||
font-size: 28px;
|
||||
}}
|
||||
|
||||
.description {{
|
||||
margin: 0 0 30px;
|
||||
color: #9da4ae;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
|
||||
label {{
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
|
||||
color: #c7ccd3;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
input {{
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
|
||||
border: 1px solid #343a43;
|
||||
border-radius: 8px;
|
||||
|
||||
background: #0f1216;
|
||||
color: #e8eaed;
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
outline: none;
|
||||
}}
|
||||
|
||||
input:focus {{
|
||||
border-color: #6b7280;
|
||||
}}
|
||||
|
||||
button {{
|
||||
width: 100%;
|
||||
margin-top: 22px;
|
||||
padding: 13px 16px;
|
||||
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
|
||||
background: #e8eaed;
|
||||
color: #111318;
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
|
||||
cursor: pointer;
|
||||
}}
|
||||
|
||||
button:hover {{
|
||||
background: #ffffff;
|
||||
}}
|
||||
|
||||
.requirements {{
|
||||
margin-top: 10px;
|
||||
|
||||
color: #6f7782;
|
||||
font-size: 13px;
|
||||
}}
|
||||
|
||||
.footer {{
|
||||
margin-top: 28px;
|
||||
padding-top: 20px;
|
||||
|
||||
border-top: 1px solid #272c33;
|
||||
|
||||
color: #6f7782;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="card">
|
||||
|
||||
<h1>Reset password</h1>
|
||||
|
||||
<form method="post" action="/api/update-password">
|
||||
<p class="description">
|
||||
Enter a new password for your account.
|
||||
</p>
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action="/update-password"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="token"
|
||||
value="{token}"
|
||||
>
|
||||
|
||||
<label>
|
||||
New password:
|
||||
<input
|
||||
type="password"
|
||||
name="new_password"
|
||||
minlength="8"
|
||||
required
|
||||
>
|
||||
<label for="new_password">
|
||||
New password
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="new_password"
|
||||
type="password"
|
||||
name="new_password"
|
||||
minlength="8"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
>
|
||||
|
||||
<div class="requirements">
|
||||
Minimum 8 characters.
|
||||
</div>
|
||||
|
||||
<button type="submit">
|
||||
Reset password
|
||||
Update password
|
||||
</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
<div class="footer">
|
||||
Calendar API
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
)
|
||||
|
||||
@app.post(
|
||||
"/update-password",
|
||||
response_class=HTMLResponse,
|
||||
summary="Reset password",
|
||||
)
|
||||
def update_password(
|
||||
token: str = Form(...),
|
||||
@@ -1194,49 +1622,219 @@ def update_password(
|
||||
)
|
||||
|
||||
if result["status"] == "failed":
|
||||
|
||||
if result["reason"] == "token_expired":
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Reset expired</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Reset link expired</h1>
|
||||
<p>This password reset link has expired.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(
|
||||
content="""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Reset failed</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Reset failed</h1>
|
||||
<p>This password reset link is invalid.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
<title>Reset link expired</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
|
||||
background: #0b0d10;
|
||||
color: #e8eaed;
|
||||
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 40px;
|
||||
|
||||
background: #15181d;
|
||||
border: 1px solid #2a2f36;
|
||||
border-radius: 14px;
|
||||
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.45);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #9da4ae;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="card">
|
||||
<h1>Reset link expired</h1>
|
||||
|
||||
<p>
|
||||
This password reset link has expired.
|
||||
Please request a new password reset.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=410,
|
||||
)
|
||||
|
||||
return HTMLResponse(
|
||||
content="""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Reset failed</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
|
||||
background: #0b0d10;
|
||||
color: #e8eaed;
|
||||
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 40px;
|
||||
|
||||
background: #15181d;
|
||||
border: 1px solid #2a2f36;
|
||||
border-radius: 14px;
|
||||
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.45);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #9da4ae;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="card">
|
||||
<h1>Reset failed</h1>
|
||||
|
||||
<p>
|
||||
This password reset link is invalid or has already been used.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
return HTMLResponse(
|
||||
content="""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Password updated</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
|
||||
background: #0b0d10;
|
||||
color: #e8eaed;
|
||||
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 40px;
|
||||
|
||||
background: #15181d;
|
||||
border: 1px solid #2a2f36;
|
||||
border-radius: 14px;
|
||||
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.45);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 24px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border-radius: 50%;
|
||||
background: #14271d;
|
||||
color: #4ade80;
|
||||
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #9da4ae;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="card">
|
||||
<div class="icon">✓</div>
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Password updated</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Password updated</h1>
|
||||
<p>Your password has been successfully changed.</p>
|
||||
<p>You may now close this tab.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
<p>
|
||||
Your password has been successfully changed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You may now close this tab.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
)
|
||||
|
||||
# easter egg
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ ROLE_LEVELS = {
|
||||
}
|
||||
|
||||
|
||||
def get_role(room_id: int, user_id: int) -> str | None:
|
||||
def get_role(room_id: int, user_id: int) -> int | None:
|
||||
member = fetch_one(
|
||||
"""
|
||||
SELECT role
|
||||
@@ -22,8 +22,7 @@ def get_role(room_id: int, user_id: int) -> str | None:
|
||||
if member is None:
|
||||
return None
|
||||
|
||||
return member["role"]
|
||||
|
||||
return ROLE_LEVELS.get(member["role"])
|
||||
|
||||
def has_role(
|
||||
room_id: int,
|
||||
@@ -36,12 +35,11 @@ def has_role(
|
||||
Role hierarchy:
|
||||
owner > admin > member
|
||||
"""
|
||||
user_role = get_role(room_id, user_id)
|
||||
user_level = get_role(room_id, user_id)
|
||||
|
||||
if user_role is None:
|
||||
if user_level is None:
|
||||
return False
|
||||
|
||||
user_level = ROLE_LEVELS.get(user_role, 0)
|
||||
required_level = ROLE_LEVELS.get(required_role, 0)
|
||||
|
||||
if required_level == 0:
|
||||
|
||||
@@ -4,3 +4,5 @@ python-dotenv
|
||||
fastapi
|
||||
pydantic
|
||||
uvicorn
|
||||
email-validator
|
||||
python-multipart
|
||||
|
||||
Reference in New Issue
Block a user