Compare commits
80
Commits
3719bb37e0
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfbd561763 | ||
|
|
fcb6690af5 | ||
|
|
08e02dd556 | ||
|
|
a980812137 | ||
|
|
e61750f314 | ||
|
|
af1c147fd0 | ||
|
|
792ce850b6 | ||
|
|
fae25adb99 | ||
|
|
8fae1ffb3d | ||
|
|
bfab69cbb4 | ||
|
|
468c34ff92 | ||
|
|
8fe812e8e6 | ||
|
|
bf601846db | ||
|
|
1822dea7e3 | ||
|
|
63bfaf9619 | ||
|
|
bbb784c3f1 | ||
|
|
3729d965f2 | ||
|
|
12fb198c46 | ||
|
|
6e7d32ea19 | ||
|
|
543650b151 | ||
|
|
87b474ac47 | ||
|
|
11b99bc2ac | ||
|
|
062d471fc3 | ||
|
|
34416ad804 | ||
|
|
662baca9e6 | ||
|
|
97fd931c32 | ||
|
|
4fac3f91b6 | ||
|
|
d4be4933f8 | ||
|
|
fcfa409a09 | ||
|
|
825ff19af7 | ||
|
|
5b5f118057 | ||
|
|
5d13f1e426 | ||
|
|
a39d824e7c | ||
|
|
f1cdb0ecf8 | ||
|
|
d2a6e14c5f | ||
|
|
0cec991cbc | ||
|
|
ab539bb368 | ||
|
|
c39286afc1 | ||
|
|
63b4ee2a6f | ||
|
|
3d3270988c | ||
|
|
54581e2e42 | ||
|
|
edada21b4d | ||
|
|
405241c1e8 | ||
|
|
fda883a89a | ||
|
|
9661305d81 | ||
|
|
f7c906c63b | ||
|
|
49e0ea5734 | ||
|
|
953c371e48 | ||
|
|
5f77a4d995 | ||
|
|
b85b8dbb25 | ||
|
|
a3f252c8d6 | ||
|
|
1f835ba4e8 | ||
|
|
967e139e43 | ||
|
|
80dc6c37c7 | ||
|
|
c11162fcb3 | ||
|
|
2c66d6b075 | ||
|
|
e0eb2d5d8f | ||
|
|
139e17114a | ||
|
|
996372c89b | ||
|
|
a585a6fa17 | ||
|
|
c527a3625e | ||
|
|
4e6d58386f | ||
|
|
a8a3ce7399 | ||
|
|
80f7f2978d | ||
|
|
d47a46c81e | ||
|
|
371b6364fe | ||
|
|
bbc8627867 | ||
|
|
fca7689d49 | ||
|
|
73bd655898 | ||
|
|
5a9b820bc6 | ||
|
|
36a6c47081 | ||
|
|
80e46c09a3 | ||
|
|
f43c32c72b | ||
|
|
6cc1f71a85 | ||
|
|
215cc47cc3 | ||
|
|
9c97f2dab7 | ||
|
|
1adafcbfb0 | ||
|
|
40ffb103ed | ||
|
|
57757e90aa | ||
|
|
5102263492 |
+27
-6
@@ -1,6 +1,27 @@
|
|||||||
.env
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
# dependencies
|
||||||
database/calendar.db
|
/node_modules
|
||||||
database/calendar.db-journal
|
|
||||||
.vscode
|
# 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.
+266
-10
@@ -1,18 +1,27 @@
|
|||||||
from database import execute, fetch_one
|
from database import execute, fetch_one, fetch_all
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from jose import jwt, JWTError
|
from jose import jwt
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from fastapi import Header, HTTPException
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import os
|
import os
|
||||||
from fastapi import Depends, HTTPException
|
from fastapi import Depends
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from email_service import (
|
||||||
|
create_verification,
|
||||||
|
send_verification_email,
|
||||||
|
send_password_reset_email,
|
||||||
|
create_password_reset,
|
||||||
|
hash_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
import secrets
|
||||||
|
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
|
CHEESE_IN_DUTCH = "kaas"
|
||||||
|
|
||||||
|
|
||||||
def create_token(user_id):
|
def create_token(user_id):
|
||||||
@@ -30,7 +39,7 @@ def create_token(user_id):
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def create_user(username, password):
|
def create_user(username, password, email):
|
||||||
|
|
||||||
password_hash = bcrypt.hashpw(
|
password_hash = bcrypt.hashpw(
|
||||||
password.encode(),
|
password.encode(),
|
||||||
@@ -39,15 +48,30 @@ def create_user(username, password):
|
|||||||
|
|
||||||
user_id = execute(
|
user_id = execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO users (username, password_hash)
|
INSERT INTO users (
|
||||||
VALUES (?, ?)
|
username,
|
||||||
|
password_hash,
|
||||||
|
email,
|
||||||
|
email_verified
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, 0)
|
||||||
""",
|
""",
|
||||||
(username, password_hash)
|
(
|
||||||
|
username,
|
||||||
|
password_hash,
|
||||||
|
email,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
token = create_verification(user_id)
|
||||||
|
|
||||||
|
send_verification_email(
|
||||||
|
email,
|
||||||
|
token,
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
def verify_user(username, password):
|
def verify_user(username, password):
|
||||||
|
|
||||||
user = fetch_one(
|
user = fetch_one(
|
||||||
@@ -78,4 +102,236 @@ def get_current_user(
|
|||||||
algorithms=[ALGORITHM]
|
algorithms=[ALGORITHM]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print("DEBUG JWT payload:", payload)
|
||||||
|
|
||||||
return payload["user_id"]
|
return payload["user_id"]
|
||||||
|
|
||||||
|
def get_user(user_id):
|
||||||
|
return fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT id, username, email, email_verified
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
def change_password(user_id: int, old_password: str, new_password: str):
|
||||||
|
user = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT password_hash
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"reason": "user_not_found",
|
||||||
|
}
|
||||||
|
|
||||||
|
if not bcrypt.checkpw(
|
||||||
|
old_password.encode(),
|
||||||
|
user["password_hash"].encode(),
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"reason": "incorrect_password",
|
||||||
|
}
|
||||||
|
|
||||||
|
password_hash = bcrypt.hashpw(
|
||||||
|
new_password.encode(),
|
||||||
|
bcrypt.gensalt(),
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
password_hash,
|
||||||
|
user_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "updated",
|
||||||
|
}
|
||||||
|
|
||||||
|
def delete_me(user_id, password):
|
||||||
|
user = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT password_hash
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"reason": "user_not_found",
|
||||||
|
}
|
||||||
|
|
||||||
|
if not bcrypt.checkpw(
|
||||||
|
password.encode(),
|
||||||
|
user["password_hash"].encode(),
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"reason": "incorrect_password",
|
||||||
|
}
|
||||||
|
|
||||||
|
owned_rooms = fetch_all(
|
||||||
|
"""
|
||||||
|
SELECT id
|
||||||
|
FROM rooms
|
||||||
|
WHERE owner_id = ?
|
||||||
|
""",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
for room in owned_rooms:
|
||||||
|
room_id = room["id"]
|
||||||
|
|
||||||
|
new_owner = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT user_id
|
||||||
|
FROM room_members
|
||||||
|
WHERE room_id = ?
|
||||||
|
AND user_id != ?
|
||||||
|
ORDER BY rowid
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(room_id, user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if new_owner:
|
||||||
|
new_owner_id = new_owner["user_id"]
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE rooms
|
||||||
|
SET owner_id = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(new_owner_id, room_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE room_members
|
||||||
|
SET role = 'owner'
|
||||||
|
WHERE room_id = ?
|
||||||
|
AND user_id = ?
|
||||||
|
""",
|
||||||
|
(room_id, new_owner_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "deleted"
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset_password(email: str):
|
||||||
|
email = email.strip().lower()
|
||||||
|
|
||||||
|
user = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT id
|
||||||
|
FROM users
|
||||||
|
WHERE email = ?
|
||||||
|
""",
|
||||||
|
(email,),
|
||||||
|
)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return {
|
||||||
|
"status": "sent",
|
||||||
|
}
|
||||||
|
|
||||||
|
token = create_password_reset(user["id"])
|
||||||
|
|
||||||
|
send_password_reset_email(
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "sent",
|
||||||
|
}
|
||||||
|
|
||||||
|
def reset_password_with_token(token: str, new_password: str):
|
||||||
|
token_hash = hash_token(token)
|
||||||
|
|
||||||
|
reset = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT id, user_id, expires_at
|
||||||
|
FROM password_reset_tokens
|
||||||
|
WHERE token_hash = ?
|
||||||
|
""",
|
||||||
|
(token_hash,),
|
||||||
|
)
|
||||||
|
|
||||||
|
if reset is None:
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"reason": "invalid_token",
|
||||||
|
}
|
||||||
|
|
||||||
|
expires_at = datetime.fromisoformat(reset["expires_at"])
|
||||||
|
|
||||||
|
if datetime.now(timezone.utc) >= expires_at:
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM password_reset_tokens
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(reset["id"],),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"reason": "token_expired",
|
||||||
|
}
|
||||||
|
|
||||||
|
password_hash = bcrypt.hashpw(
|
||||||
|
new_password.encode(),
|
||||||
|
bcrypt.gensalt(),
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
password_hash,
|
||||||
|
reset["user_id"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM password_reset_tokens
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(reset["id"],),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "updated",
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
curl -X POST http://127.0.0.1:8000/events -H "Content-Type: application/json" -d '{
|
|
||||||
"room_id": 1,
|
|
||||||
"creator_id": 2,
|
|
||||||
"type": "proposal",
|
|
||||||
"title": "Filmavond",
|
|
||||||
"description": "Samen een film kijken en snacks halen",
|
|
||||||
"start_time": "2026-08-15 20:00",
|
|
||||||
"end_time": "2026-08-15 23:00",
|
|
||||||
"visibility": "room",
|
|
||||||
"status": "proposed",
|
|
||||||
"min_people": 4
|
|
||||||
}'
|
|
||||||
+10
-12
@@ -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';")
|
||||||
@@ -73,3 +61,13 @@ if __name__ == "__main__":
|
|||||||
print("-", table["name"])
|
print("-", table["name"])
|
||||||
else:
|
else:
|
||||||
print("Geen tabellen gevonden.")
|
print("Geen tabellen gevonden.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_table_names():
|
||||||
|
return fetch_all(
|
||||||
|
"""
|
||||||
|
SELECT name
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = 'table'
|
||||||
|
"""
|
||||||
|
)
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
import hashlib
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
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 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")
|
||||||
|
|
||||||
|
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()
|
||||||
|
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 create_password_reset(
|
||||||
|
user_id: int,
|
||||||
|
):
|
||||||
|
token = secrets.token_urlsafe(32)
|
||||||
|
token_hash = hash_token(token)
|
||||||
|
|
||||||
|
expires_at = (
|
||||||
|
datetime.now(timezone.utc)
|
||||||
|
+ timedelta(hours=1)
|
||||||
|
).isoformat()
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM password_reset_tokens
|
||||||
|
WHERE user_id = ?
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO password_reset_tokens (
|
||||||
|
user_id,
|
||||||
|
token_hash,
|
||||||
|
expires_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
token_hash,
|
||||||
|
expires_at,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -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):
|
def respond_to_event(event_id, user_id, status):
|
||||||
@@ -53,3 +53,14 @@ def get_event_responses(event_id):
|
|||||||
""",
|
""",
|
||||||
(event_id,)
|
(event_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_event_responses(event_id):
|
||||||
|
return fetch_all(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM event_signups
|
||||||
|
WHERE event_id = ?
|
||||||
|
""",
|
||||||
|
(event_id,)
|
||||||
|
)
|
||||||
+109
-19
@@ -2,8 +2,7 @@ from database import execute
|
|||||||
from database import fetch_all, fetch_one
|
from database import fetch_all, fetch_one
|
||||||
|
|
||||||
|
|
||||||
def get_user_events(user_id):
|
def get_user_events(user_id, date_from=None, date_to=None):
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
SELECT events.*
|
SELECT events.*
|
||||||
FROM events
|
FROM events
|
||||||
@@ -12,9 +11,21 @@ def get_user_events(user_id):
|
|||||||
WHERE room_members.user_id = ?
|
WHERE room_members.user_id = ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
params = [user_id]
|
||||||
|
|
||||||
|
if date_from:
|
||||||
|
query += " AND events.start_time >= ?"
|
||||||
|
params.append(date_from)
|
||||||
|
|
||||||
|
if date_to:
|
||||||
|
query += " AND events.start_time <= ?"
|
||||||
|
params.append(date_to)
|
||||||
|
|
||||||
|
query += " ORDER BY events.start_time"
|
||||||
|
|
||||||
return fetch_all(
|
return fetch_all(
|
||||||
query,
|
query,
|
||||||
(user_id,)
|
tuple(params),
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_event(
|
def create_event(
|
||||||
@@ -135,11 +146,11 @@ def update_event(
|
|||||||
description,
|
description,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
min_people
|
min_people,
|
||||||
|
event_type,
|
||||||
|
visibility,
|
||||||
|
status,
|
||||||
):
|
):
|
||||||
|
|
||||||
from database import execute
|
|
||||||
|
|
||||||
execute(
|
execute(
|
||||||
"""
|
"""
|
||||||
UPDATE events
|
UPDATE events
|
||||||
@@ -148,7 +159,10 @@ def update_event(
|
|||||||
description = COALESCE(?, description),
|
description = COALESCE(?, description),
|
||||||
start_time = COALESCE(?, start_time),
|
start_time = COALESCE(?, start_time),
|
||||||
end_time = COALESCE(?, end_time),
|
end_time = COALESCE(?, end_time),
|
||||||
min_people = COALESCE(?, min_people)
|
min_people = COALESCE(?, min_people),
|
||||||
|
type = COALESCE(?, type),
|
||||||
|
visibility = COALESCE(?, visibility),
|
||||||
|
status = COALESCE(?, status)
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
@@ -157,20 +171,96 @@ def update_event(
|
|||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
min_people,
|
min_people,
|
||||||
event_id
|
event_type,
|
||||||
)
|
visibility,
|
||||||
|
status,
|
||||||
|
event_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_room_events(room_id):
|
def get_room_events(room_id, date_from=None, date_to=None):
|
||||||
|
query = """
|
||||||
from database import fetch_all
|
|
||||||
|
|
||||||
return fetch_all(
|
|
||||||
"""
|
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM events
|
FROM events
|
||||||
WHERE room_id = ?
|
WHERE room_id = ?
|
||||||
ORDER BY start_time
|
"""
|
||||||
""",
|
|
||||||
(room_id,)
|
params = [room_id]
|
||||||
|
|
||||||
|
if date_from:
|
||||||
|
query += " AND start_time >= ?"
|
||||||
|
params.append(date_from)
|
||||||
|
|
||||||
|
if date_to:
|
||||||
|
query += " AND start_time <= ?"
|
||||||
|
params.append(date_to)
|
||||||
|
|
||||||
|
query += " ORDER BY start_time"
|
||||||
|
|
||||||
|
return fetch_all(
|
||||||
|
query,
|
||||||
|
tuple(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_event(event_id):
|
||||||
|
return fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT *
|
||||||
|
FROM events
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(event_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_event_room_id(event_id):
|
||||||
|
event = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT room_id
|
||||||
|
FROM events
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(event_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
if event is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return event["room_id"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_event_creator(event_id):
|
||||||
|
event = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT creator_id
|
||||||
|
FROM events
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(event_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
if event is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return event["creator_id"]
|
||||||
|
|
||||||
|
def confirm_event(event_id):
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE events
|
||||||
|
SET status = 'confirmed'
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(event_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_event(event_id):
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE events
|
||||||
|
SET status = 'cancelled'
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(event_id,),
|
||||||
)
|
)
|
||||||
+1570
-439
File diff suppressed because it is too large
Load Diff
+87
-14
@@ -1,7 +1,14 @@
|
|||||||
from database import fetch_one
|
from database import fetch_one
|
||||||
|
|
||||||
|
|
||||||
def get_room_role(room_id, user_id):
|
ROLE_LEVELS = {
|
||||||
|
"member": 1,
|
||||||
|
"admin": 2,
|
||||||
|
"owner": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_role(room_id: int, user_id: int) -> int | None:
|
||||||
member = fetch_one(
|
member = fetch_one(
|
||||||
"""
|
"""
|
||||||
SELECT role
|
SELECT role
|
||||||
@@ -9,26 +16,92 @@ def get_room_role(room_id, user_id):
|
|||||||
WHERE room_id = ?
|
WHERE room_id = ?
|
||||||
AND user_id = ?
|
AND user_id = ?
|
||||||
""",
|
""",
|
||||||
(
|
(room_id, user_id),
|
||||||
room_id,
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if member is None:
|
if member is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return member["role"]
|
return ROLE_LEVELS.get(member["role"])
|
||||||
|
|
||||||
|
def has_role(
|
||||||
|
room_id: int,
|
||||||
|
user_id: int,
|
||||||
|
required_role: str,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether a user has at least the required role.
|
||||||
|
|
||||||
def has_role(room_id, user_id, allowed_roles):
|
Role hierarchy:
|
||||||
|
owner > admin > member
|
||||||
|
"""
|
||||||
|
user_level = get_role(room_id, user_id)
|
||||||
|
|
||||||
role = get_room_role(
|
if user_level is None:
|
||||||
room_id,
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if role is None:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return role in allowed_roles
|
required_level = ROLE_LEVELS.get(required_role, 0)
|
||||||
|
|
||||||
|
if required_level == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return user_level >= required_level
|
||||||
|
|
||||||
|
|
||||||
|
def whoami(room_id: int, user_id: int) -> dict:
|
||||||
|
"""
|
||||||
|
Return the user's permissions within a room.
|
||||||
|
"""
|
||||||
|
role = get_role(room_id, user_id)
|
||||||
|
|
||||||
|
if role is None:
|
||||||
|
return {
|
||||||
|
"member": False,
|
||||||
|
"role": None,
|
||||||
|
"is_admin": False,
|
||||||
|
"is_owner": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"member": True,
|
||||||
|
"role": role,
|
||||||
|
"is_admin": ROLE_LEVELS.get(role, 0) >= ROLE_LEVELS["admin"],
|
||||||
|
"is_owner": role == "owner",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# PUBLIC
|
||||||
|
# ├── POST /create-user
|
||||||
|
# ├── POST /login
|
||||||
|
# ├── GET /
|
||||||
|
# └── GET /ascii-art
|
||||||
|
|
||||||
|
# AUTHENTICATED
|
||||||
|
# ├── GET /me
|
||||||
|
# ├── GET /users/{id}
|
||||||
|
# ├── GET /database
|
||||||
|
# ├── POST /create-room
|
||||||
|
# ├── POST /search-room
|
||||||
|
# ├── POST /join-room
|
||||||
|
# └── GET /rooms
|
||||||
|
|
||||||
|
# MEMBER
|
||||||
|
# ├── GET /rooms/{id}
|
||||||
|
# ├── GET /rooms/{id}/whoami
|
||||||
|
# ├── GET /rooms/{id}/members
|
||||||
|
# ├── GET /rooms/{id}/events
|
||||||
|
# ├── DELETE /rooms/{id}/members/me
|
||||||
|
# ├── POST /events
|
||||||
|
# ├── GET /events
|
||||||
|
# ├── GET /events/{id}
|
||||||
|
# ├── POST /events/{id}/respond
|
||||||
|
# └── GET /events/{id}/responses
|
||||||
|
|
||||||
|
# ADMIN
|
||||||
|
# ├── PATCH /rooms/{id}
|
||||||
|
# ├── POST /rooms/{id}/invite-code
|
||||||
|
# ├── PATCH /events/{id}
|
||||||
|
# └── DELETE /events/{id}
|
||||||
|
|
||||||
|
# OWNER
|
||||||
|
# └── DELETE /rooms/{id}
|
||||||
+186
-53
@@ -1,10 +1,29 @@
|
|||||||
from fastapi import FastAPI
|
import re
|
||||||
from pydantic import BaseModel
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
from database import execute, fetch_one, fetch_all
|
from database import execute, fetch_one, fetch_all
|
||||||
|
|
||||||
DEFAULT_ROLE = "member"
|
DEFAULT_ROLE = "member"
|
||||||
|
|
||||||
def create_room(user_id, room_name, invite_code):
|
|
||||||
|
def validate_invite_code(invite_code: str):
|
||||||
|
if not 6 <= len(invite_code) <= 32:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not re.fullmatch(r"[A-Za-z0-9]+", invite_code):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def create_room(user_id: int, room_name: str, invite_code: str):
|
||||||
|
|
||||||
|
if not validate_invite_code(invite_code):
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"reason": "invalid_invite_code",
|
||||||
|
}
|
||||||
|
|
||||||
room_id = execute(
|
room_id = execute(
|
||||||
"""
|
"""
|
||||||
@@ -14,7 +33,7 @@ def create_room(user_id, room_name, invite_code):
|
|||||||
(
|
(
|
||||||
room_name,
|
room_name,
|
||||||
user_id,
|
user_id,
|
||||||
invite_code
|
invite_code,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,28 +45,25 @@ def create_room(user_id, room_name, invite_code):
|
|||||||
(
|
(
|
||||||
room_id,
|
room_id,
|
||||||
user_id,
|
user_id,
|
||||||
"owner"
|
"owner",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"room_id": room_id,
|
"room_id": room_id,
|
||||||
"member_id": member_id
|
"member_id": member_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
def search_room(
|
|
||||||
invite_code
|
def search_room(invite_code: str):
|
||||||
):
|
|
||||||
query = """
|
room = fetch_one(
|
||||||
|
"""
|
||||||
SELECT name
|
SELECT name
|
||||||
FROM rooms
|
FROM rooms
|
||||||
WHERE invite_code = ?
|
WHERE invite_code = ?
|
||||||
"""
|
""",
|
||||||
room = fetch_one(
|
(invite_code,)
|
||||||
query,
|
|
||||||
(
|
|
||||||
invite_code,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if room is None:
|
if room is None:
|
||||||
@@ -56,16 +72,14 @@ def search_room(
|
|||||||
return room["name"]
|
return room["name"]
|
||||||
|
|
||||||
|
|
||||||
def accept_room(user_id, invite_code):
|
def accept_room(user_id: int, invite_code: str):
|
||||||
|
|
||||||
query = """
|
room = fetch_one(
|
||||||
|
"""
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM rooms
|
FROM rooms
|
||||||
WHERE invite_code = ?
|
WHERE invite_code = ?
|
||||||
"""
|
""",
|
||||||
|
|
||||||
room = fetch_one(
|
|
||||||
query,
|
|
||||||
(invite_code,)
|
(invite_code,)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,36 +97,36 @@ def accept_room(user_id, invite_code):
|
|||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
room_id,
|
room_id,
|
||||||
user_id
|
user_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
member_id = execute(
|
||||||
query = """
|
"""
|
||||||
INSERT INTO room_members
|
INSERT INTO room_members
|
||||||
(room_id, user_id, role)
|
(room_id, user_id, role)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
"""
|
""",
|
||||||
|
|
||||||
member_id = execute(
|
|
||||||
query,
|
|
||||||
(
|
(
|
||||||
room_id,
|
room_id,
|
||||||
user_id,
|
user_id,
|
||||||
"member"
|
DEFAULT_ROLE,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"room_id": room_id,
|
"room_id": room_id,
|
||||||
"member_id": member_id
|
"member_id": member_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_rooms(user_id):
|
|
||||||
query = """
|
def get_rooms(user_id: int):
|
||||||
|
|
||||||
|
return fetch_all(
|
||||||
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
rooms.id,
|
rooms.id,
|
||||||
rooms.name,
|
rooms.name,
|
||||||
@@ -123,18 +137,25 @@ def get_rooms(user_id):
|
|||||||
JOIN room_members
|
JOIN room_members
|
||||||
ON rooms.id = room_members.room_id
|
ON rooms.id = room_members.room_id
|
||||||
WHERE room_members.user_id = ?
|
WHERE room_members.user_id = ?
|
||||||
|
""",
|
||||||
|
(user_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_room(room_id: int):
|
||||||
|
|
||||||
|
room = fetch_one(
|
||||||
"""
|
"""
|
||||||
|
SELECT id
|
||||||
rooms = fetch_all(
|
FROM rooms
|
||||||
query,
|
WHERE id = ?
|
||||||
(
|
""",
|
||||||
user_id,
|
(room_id,)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return rooms
|
if not room:
|
||||||
|
return {"error": "room not found"}
|
||||||
|
|
||||||
def delete_room(room_id):
|
|
||||||
execute(
|
execute(
|
||||||
"""
|
"""
|
||||||
DELETE FROM rooms
|
DELETE FROM rooms
|
||||||
@@ -143,9 +164,10 @@ def delete_room(room_id):
|
|||||||
(room_id,)
|
(room_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"status":"deleted"}
|
return {"status": "deleted"}
|
||||||
|
|
||||||
def get_room(room_id, user_id):
|
|
||||||
|
def get_room(room_id: int, user_id: int):
|
||||||
|
|
||||||
room = fetch_one(
|
room = fetch_one(
|
||||||
"""
|
"""
|
||||||
@@ -162,14 +184,13 @@ def get_room(room_id, user_id):
|
|||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
room_id,
|
room_id,
|
||||||
user_id
|
user_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if room is None:
|
if room is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
members = fetch_all(
|
members = fetch_all(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -184,7 +205,6 @@ def get_room(room_id, user_id):
|
|||||||
(room_id,)
|
(room_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
events = fetch_all(
|
events = fetch_all(
|
||||||
"""
|
"""
|
||||||
SELECT *
|
SELECT *
|
||||||
@@ -195,20 +215,21 @@ def get_room(room_id, user_id):
|
|||||||
(room_id,)
|
(room_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"room": dict(room),
|
"room": dict(room),
|
||||||
"members": [dict(member) for member in members],
|
"members": [dict(member) for member in members],
|
||||||
"events": [dict(event) for event in events]
|
"events": [dict(event) for event in events],
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_room_members(room_id):
|
|
||||||
|
def get_room_members(room_id: int):
|
||||||
|
|
||||||
return fetch_all(
|
return fetch_all(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
users.id,
|
users.id,
|
||||||
users.username
|
users.username,
|
||||||
|
room_members.role
|
||||||
FROM room_members
|
FROM room_members
|
||||||
JOIN users
|
JOIN users
|
||||||
ON users.id = room_members.user_id
|
ON users.id = room_members.user_id
|
||||||
@@ -217,9 +238,8 @@ def get_room_members(room_id):
|
|||||||
(room_id,)
|
(room_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
def leave_room(room_id, user_id):
|
|
||||||
|
|
||||||
from database import execute
|
def leave_room(room_id: int, user_id: int):
|
||||||
|
|
||||||
execute(
|
execute(
|
||||||
"""
|
"""
|
||||||
@@ -229,6 +249,119 @@ def leave_room(room_id, user_id):
|
|||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
room_id,
|
room_id,
|
||||||
user_id
|
user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def change_room_name(room_id: int, new_name: str, user_id: int):
|
||||||
|
|
||||||
|
room = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT id
|
||||||
|
FROM rooms
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(room_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not room:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"reason": "room_not_found",
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE rooms
|
||||||
|
SET name = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
new_name,
|
||||||
|
room_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "updated",
|
||||||
|
"new_name": new_name,
|
||||||
|
"room_id": room_id,
|
||||||
|
"updated_by": user_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def regenerate_invite_code(room_id: int):
|
||||||
|
|
||||||
|
new_code = ''.join(
|
||||||
|
secrets.choice(string.ascii_uppercase + string.digits)
|
||||||
|
for _ in range(8)
|
||||||
|
)
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE rooms
|
||||||
|
SET invite_code = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
new_code,
|
||||||
|
room_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "updated",
|
||||||
|
"room_id": room_id,
|
||||||
|
"invite_code": new_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def remove_room_member(room_id: int, user_id: int):
|
||||||
|
|
||||||
|
member = fetch_one(
|
||||||
|
"""
|
||||||
|
SELECT 1
|
||||||
|
FROM room_members
|
||||||
|
WHERE room_id = ?
|
||||||
|
AND user_id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if member is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM room_members
|
||||||
|
WHERE room_id = ?
|
||||||
|
AND user_id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def update_member_role(room_id: int, user_id: int, role: str):
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"""
|
||||||
|
UPDATE room_members
|
||||||
|
SET role = ?
|
||||||
|
WHERE room_id = ?
|
||||||
|
AND user_id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
role,
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
+40
-12
@@ -1,21 +1,49 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo "Waar wil je de Calendar API starten?"
|
set -e
|
||||||
echo "1) Lokaal "
|
|
||||||
echo "2) LAN "
|
|
||||||
read -p "Keuze [1/2]: " choice
|
|
||||||
|
|
||||||
case $choice in
|
VENV=""
|
||||||
1)
|
|
||||||
echo "Start lokaal..."
|
for dir in .venv venv env; do
|
||||||
python3 -m uvicorn main:app --reload
|
if [ -f "$dir/bin/activate" ]; then
|
||||||
|
VENV="$dir"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$VENV" ]; then
|
||||||
|
echo "ERROR: No virtual environment found."
|
||||||
|
echo "Expected .venv/, venv/ or env/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using virtual environment: $VENV"
|
||||||
|
|
||||||
|
source "$VENV/bin/activate"
|
||||||
|
|
||||||
|
if [ ! -f requirements.txt ]; then
|
||||||
|
echo "ERROR: requirements.txt not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking requirements..."
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
local)
|
||||||
|
echo "Starting Calendar API locally..."
|
||||||
|
python -m uvicorn main:app --reload
|
||||||
;;
|
;;
|
||||||
2)
|
|
||||||
echo "Start op LAN..."
|
lan)
|
||||||
python3 -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
echo "Starting Calendar API on LAN..."
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
;;
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
echo "Ongeldige keuze"
|
echo "Usage: $0 {local|lan}"
|
||||||
|
echo " local Start locally"
|
||||||
|
echo " lan Start on LAN"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
+109
-17
@@ -1,12 +1,13 @@
|
|||||||
import sqlite3
|
#!/bin/python3
|
||||||
|
|
||||||
DB_NAME = "calendar.db"
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DB_NAME = Path(__file__).parent / "calendar.db"
|
||||||
|
|
||||||
conn = sqlite3.connect(DB_NAME)
|
conn = sqlite3.connect(DB_NAME)
|
||||||
|
|
||||||
# SQLite foreign keys activeren
|
|
||||||
conn.execute("PRAGMA foreign_keys = ON;")
|
conn.execute("PRAGMA foreign_keys = ON;")
|
||||||
|
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
|
||||||
@@ -14,11 +15,43 @@ cur.execute("""
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
email TEXT UNIQUE,
|
||||||
|
email_verified INTEGER NOT NULL DEFAULT 0,
|
||||||
password_hash TEXT NOT NULL
|
password_hash TEXT NOT NULL
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS email_verifications (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS rooms (
|
CREATE TABLE IF NOT EXISTS rooms (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -26,7 +59,8 @@ CREATE TABLE IF NOT EXISTS rooms (
|
|||||||
owner_id INTEGER NOT NULL,
|
owner_id INTEGER NOT NULL,
|
||||||
invite_code TEXT NOT NULL UNIQUE,
|
invite_code TEXT NOT NULL UNIQUE,
|
||||||
|
|
||||||
FOREIGN KEY (owner_id) REFERENCES users(id)
|
FOREIGN KEY (owner_id)
|
||||||
|
REFERENCES users(id)
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@@ -54,7 +88,6 @@ CREATE TABLE IF NOT EXISTS room_members (
|
|||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS events (
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|
||||||
room_id INTEGER NOT NULL,
|
room_id INTEGER NOT NULL,
|
||||||
creator_id INTEGER NOT NULL,
|
creator_id INTEGER NOT NULL,
|
||||||
|
|
||||||
@@ -75,6 +108,9 @@ CREATE TABLE IF NOT EXISTS events (
|
|||||||
|
|
||||||
min_people INTEGER DEFAULT 0,
|
min_people INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
invitation_mode TEXT NOT NULL DEFAULT 'room'
|
||||||
|
CHECK(invitation_mode IN ('room', 'custom')),
|
||||||
|
|
||||||
FOREIGN KEY (room_id)
|
FOREIGN KEY (room_id)
|
||||||
REFERENCES rooms(id)
|
REFERENCES rooms(id)
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
@@ -85,6 +121,29 @@ CREATE TABLE IF NOT EXISTS events (
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS event_invitations (
|
||||||
|
event_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
CHECK(status IN ('pending', 'accepted', 'declined')),
|
||||||
|
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY (event_id, user_id),
|
||||||
|
|
||||||
|
FOREIGN KEY (event_id)
|
||||||
|
REFERENCES events(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS event_signups (
|
CREATE TABLE IF NOT EXISTS event_signups (
|
||||||
event_id INTEGER NOT NULL,
|
event_id INTEGER NOT NULL,
|
||||||
@@ -93,40 +152,73 @@ CREATE TABLE IF NOT EXISTS event_signups (
|
|||||||
status TEXT NOT NULL DEFAULT 'going'
|
status TEXT NOT NULL DEFAULT 'going'
|
||||||
CHECK(status IN ('going', 'maybe', 'declined')),
|
CHECK(status IN ('going', 'maybe', 'declined')),
|
||||||
|
|
||||||
PRIMARY KEY(event_id, user_id),
|
PRIMARY KEY (event_id, user_id),
|
||||||
|
|
||||||
FOREIGN KEY(event_id)
|
FOREIGN KEY (event_id)
|
||||||
REFERENCES events(id)
|
REFERENCES events(id)
|
||||||
ON DELETE CASCADE,
|
ON DELETE CASCADE,
|
||||||
|
|
||||||
FOREIGN KEY(user_id)
|
FOREIGN KEY (user_id)
|
||||||
REFERENCES users(id)
|
REFERENCES users(id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
# Snellere zoekopdrachten
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE INDEX IF NOT EXISTS idx_room_members_user
|
CREATE INDEX IF NOT EXISTS idx_room_members_user
|
||||||
ON room_members(user_id);
|
ON room_members(user_id);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE INDEX IF NOT EXISTS idx_events_room
|
CREATE INDEX IF NOT EXISTS idx_events_room
|
||||||
ON events(room_id);
|
ON events(room_id);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_event_invitations_user
|
||||||
|
ON event_invitations(user_id);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_event_invitations_event
|
||||||
|
ON event_invitations(event_id);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_event_signups_user
|
||||||
|
ON event_signups(user_id);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_verifications_user
|
||||||
|
ON email_verifications(user_id);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user
|
||||||
|
ON password_reset_tokens(user_id);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
print("Database initialized.")
|
||||||
|
print(f"Database: {DB_NAME}")
|
||||||
|
print("Tables:")
|
||||||
|
|
||||||
print("Database aangemaakt.")
|
for row in cur.execute("""
|
||||||
print("Tabellen:")
|
SELECT name
|
||||||
|
FROM sqlite_master
|
||||||
for row in cur.execute(
|
WHERE type = 'table'
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
ORDER BY name;
|
||||||
):
|
"""):
|
||||||
print(" -", row[0])
|
print(" -", row[0])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/python3
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DB_NAME = Path(__file__).parent / "calendar.db"
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(DB_NAME)
|
||||||
|
conn.execute("PRAGMA foreign_keys = ON;")
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Migrations
|
||||||
|
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("Database updated.")
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
print(f"Database error: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.rollback()
|
||||||
|
conn.close()
|
||||||
|
except (NameError, sqlite3.Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.rollback()
|
||||||
|
conn.close()
|
||||||
|
except (NameError, sqlite3.Error):
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise SystemExit(1)
|
||||||
-1144
File diff suppressed because it is too large
Load Diff
@@ -1,100 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Calendar Dashboard</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="login-screen" id="login-screen">
|
|
||||||
|
|
||||||
<div class="login-box">
|
|
||||||
|
|
||||||
<h2>Calendar</h2>
|
|
||||||
|
|
||||||
<form onsubmit="handleLogin(event)">
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="login-username"
|
|
||||||
placeholder="Username"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="login-password"
|
|
||||||
placeholder="Password"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
|
|
||||||
<button type="submit">Log in</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<button onclick="handleRegister(event)">
|
|
||||||
Create account
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p id="login-error" class="error"></p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="layout" id="app">
|
|
||||||
|
|
||||||
<aside class="sidebar">
|
|
||||||
|
|
||||||
<div class="logo">
|
|
||||||
<h2>Calendar</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
|
|
||||||
<a data-page="dashboard" onclick="showPage('dashboard')" class="active">
|
|
||||||
Dashboard
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a data-page="rooms" onclick="showPage('rooms')">
|
|
||||||
Rooms
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a data-page="events" onclick="showPage('events')">
|
|
||||||
Events
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a data-page="settings" onclick="showPage('settings')">
|
|
||||||
Settings
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="sidebar-bottom">
|
|
||||||
|
|
||||||
<button onclick="toggleTheme()">
|
|
||||||
dark / light Theme
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="content" id="content">
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<script src="app.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,632 +0,0 @@
|
|||||||
:root {
|
|
||||||
|
|
||||||
--bg: #f4f5f7;
|
|
||||||
--panel: white;
|
|
||||||
--sidebar: #ffffff;
|
|
||||||
--text: #111;
|
|
||||||
--muted: #666;
|
|
||||||
--border: #ddd;
|
|
||||||
--accent: #2563eb;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
|
|
||||||
--bg: #080808;
|
|
||||||
--panel: #151515;
|
|
||||||
--sidebar: #101010;
|
|
||||||
--text: #eee;
|
|
||||||
--muted: #aaa;
|
|
||||||
--border: #333;
|
|
||||||
--accent: #38bdf8;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
body {
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
background: var(--bg);
|
|
||||||
|
|
||||||
color: var(--text);
|
|
||||||
|
|
||||||
font-family:
|
|
||||||
Inter,
|
|
||||||
system-ui,
|
|
||||||
sans-serif;
|
|
||||||
|
|
||||||
transition: 0.2s;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.layout {
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
|
|
||||||
grid-template-columns: 260px 1fr;
|
|
||||||
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
|
|
||||||
background: var(--sidebar);
|
|
||||||
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
|
|
||||||
padding: 25px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
margin-bottom: 40px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.logo h2 {
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
nav {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
padding: 12px 15px;
|
|
||||||
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
color: var(--muted);
|
|
||||||
|
|
||||||
transition: 0.2s;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
nav a:hover,
|
|
||||||
nav .active {
|
|
||||||
|
|
||||||
background: var(--accent);
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.sidebar-bottom {
|
|
||||||
|
|
||||||
margin-top: auto;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
button {
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
|
|
||||||
background: var(--panel);
|
|
||||||
|
|
||||||
color: var(--text);
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
margin-top: 8px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Content */
|
|
||||||
|
|
||||||
.content {
|
|
||||||
|
|
||||||
padding: 40px;
|
|
||||||
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
|
|
||||||
margin-top: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.card {
|
|
||||||
|
|
||||||
background: var(--panel);
|
|
||||||
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
|
|
||||||
border-radius: 16px;
|
|
||||||
|
|
||||||
padding: 25px;
|
|
||||||
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
box-shadow:
|
|
||||||
0 10px 30px rgba(0,0,0,0.05);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* forms */
|
|
||||||
|
|
||||||
input {
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
|
|
||||||
background: var(--bg);
|
|
||||||
|
|
||||||
color: var(--text);
|
|
||||||
|
|
||||||
margin-top: 8px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
form {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.muted {
|
|
||||||
|
|
||||||
color: var(--muted);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.error {
|
|
||||||
|
|
||||||
color: #e11d48;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* list items */
|
|
||||||
|
|
||||||
.list-item {
|
|
||||||
|
|
||||||
padding: 10px 15px;
|
|
||||||
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* event actions */
|
|
||||||
|
|
||||||
.event-actions {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
margin-top: 15px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.event-actions button {
|
|
||||||
|
|
||||||
margin-top: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* calendar */
|
|
||||||
|
|
||||||
.calendar-header {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-header h3 {
|
|
||||||
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
text-transform: capitalize;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-nav-btn {
|
|
||||||
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
padding: 8px 14px;
|
|
||||||
|
|
||||||
margin-top: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-legend {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
gap: 18px;
|
|
||||||
|
|
||||||
margin-top: 14px;
|
|
||||||
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
color: var(--muted);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.legend-item {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
gap: 6px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.legend-dot {
|
|
||||||
|
|
||||||
width: 10px;
|
|
||||||
|
|
||||||
height: 10px;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* week grid */
|
|
||||||
|
|
||||||
.calendar-week {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
margin-top: 16px;
|
|
||||||
|
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-gutter {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: 55px;
|
|
||||||
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-hour-label {
|
|
||||||
|
|
||||||
font-size: 11px;
|
|
||||||
|
|
||||||
color: var(--muted);
|
|
||||||
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
padding-right: 8px;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
transform: translateY(-6px);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-days {
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
min-width: 700px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-day-column {
|
|
||||||
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
border-left: 1px solid var(--border);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-day-header {
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
padding: 6px 0 10px 0;
|
|
||||||
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
color: var(--muted);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-day-header.today .calendar-day-number {
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
background: var(--accent);
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
padding: 2px 7px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-day-name {
|
|
||||||
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
font-size: 10px;
|
|
||||||
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-day-number {
|
|
||||||
|
|
||||||
font-size: 14px;
|
|
||||||
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
color: var(--text);
|
|
||||||
|
|
||||||
margin-top: 2px;
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-day-track {
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
background-image:
|
|
||||||
repeating-linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
var(--border) 0,
|
|
||||||
var(--border) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 48px
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-event {
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
padding: 3px 6px;
|
|
||||||
|
|
||||||
font-size: 11px;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
|
||||||
|
|
||||||
opacity: 0.92;
|
|
||||||
|
|
||||||
border: 1px solid rgba(255,255,255,0.4);
|
|
||||||
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-event:hover {
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
z-index: 99 !important;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-event-time {
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
font-size: 10px;
|
|
||||||
|
|
||||||
opacity: 0.85;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.calendar-event-title {
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.event-confirmed {
|
|
||||||
|
|
||||||
background: #16a34a;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.event-busy {
|
|
||||||
|
|
||||||
background: #f97316;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.event-private {
|
|
||||||
|
|
||||||
background: #6b7280;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* login screen */
|
|
||||||
|
|
||||||
.login-screen {
|
|
||||||
|
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.login-box {
|
|
||||||
|
|
||||||
background: var(--panel);
|
|
||||||
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
|
|
||||||
border-radius: 16px;
|
|
||||||
|
|
||||||
padding: 40px;
|
|
||||||
|
|
||||||
width: 320px;
|
|
||||||
|
|
||||||
box-shadow:
|
|
||||||
0 10px 30px rgba(0,0,0,0.05);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.login-box h2 {
|
|
||||||
|
|
||||||
margin-top: 0;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
}
|
|
||||||
Regular → Executable
+15
-10
@@ -1,13 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
# git-commit-safe - Add all and commit with a message (no push)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
cd ~/Desktop/calender || exit
|
msg="${1:-}"
|
||||||
|
if [[ -z "$msg" ]]; then
|
||||||
|
echo "Usage: $0 \"commit message\""
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
git add .
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No staged changes to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
read -p "commit msg: " COMMITMSG
|
git commit -m "$msg"
|
||||||
|
echo "Committed with message: $msg"
|
||||||
git commit -m "$COMMITMSG"
|
|
||||||
|
|
||||||
git push
|
|
||||||
|
|
||||||
cd ~
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
bcrypt
|
||||||
|
python-jose
|
||||||
|
python-dotenv
|
||||||
|
fastapi
|
||||||
|
pydantic
|
||||||
|
uvicorn
|
||||||
|
email-validator
|
||||||
|
python-multipart
|
||||||
Reference in New Issue
Block a user