Compare commits

..
80 Commits
Author SHA1 Message Date
ben cfbd561763 lol 2026-08-20 17:19:22 +02:00
ben fcb6690af5 lol 2026-08-20 16:59:27 +02:00
Ben de Roo 08e02dd556 ja 2026-08-20 16:51:58 +02:00
ben a980812137 msg 2026-08-20 12:23:30 +02:00
ben e61750f314 kaas is toch zo lekker 2026-08-20 09:06:47 +02:00
ben af1c147fd0 kaas is toch zo lekker 2026-08-20 09:04:41 +02:00
ben 792ce850b6 ik hou van kaas 2026-08-19 14:17:30 +02:00
ben fae25adb99 ik hou van kaas 2026-08-19 12:18:53 +02:00
ben 8fae1ffb3d commit 2026-08-19 12:10:02 +02:00
ben bfab69cbb4 ik hou van kaas 2026-08-19 11:58:37 +02:00
ben 468c34ff92 ik hou van kaas 2026-08-19 11:56:44 +02:00
ben 8fe812e8e6 fixed bugs 2026-08-19 09:38:31 +02:00
ben bf601846db new frontend
ik ben zo cool
2026-08-19 09:33:48 +02:00
ben 1822dea7e3 Clean up gitignore, stop tracking build artifacts 2026-08-19 09:32:32 +02:00
ben 63bfaf9619 update 2026-08-18 19:14:30 +02:00
ben bbb784c3f1 ik hou zo veel van kaaaas
jammie
2026-08-18 19:05:38 +02:00
ben 3729d965f2 Add frontend/index.html 2026-08-18 19:04:50 +02:00
ben 12fb198c46 Delete frontend/www/style.css 2026-08-18 19:04:31 +02:00
ben 6e7d32ea19 Delete frontend/www/index.html 2026-08-18 19:04:28 +02:00
ben 543650b151 Delete frontend/www/app.js 2026-08-18 19:04:24 +02:00
ben 87b474ac47 update 2026-08-18 17:38:45 +02:00
ben 11b99bc2ac update 2026-08-18 17:38:05 +02:00
ben 062d471fc3 update 2026-08-18 17:36:15 +02:00
ben 34416ad804 update 2026-08-18 17:31:17 +02:00
ben 662baca9e6 update main.py
kaasislekker
2026-08-18 17:25:28 +02:00
ben 97fd931c32 update main.py
kaasislekker
2026-08-18 17:22:51 +02:00
ben 4fac3f91b6 update main.py
kaasislekker
2026-08-18 17:17:58 +02:00
ben d4be4933f8 update main.py
kaasislekker
2026-08-18 17:04:47 +02:00
ben fcfa409a09 update main.py
kaasislekker
2026-08-18 17:03:09 +02:00
ben 825ff19af7 update main.py
kaasislekker
2026-08-18 16:57:53 +02:00
ben 5b5f118057 update main.py
kaasislekker
2026-08-18 16:46:42 +02:00
ben 5d13f1e426 update main.py
kaasislekker
2026-08-18 15:28:05 +02:00
ben a39d824e7c update main.py
kaasislekker
2026-08-18 15:26:19 +02:00
ben f1cdb0ecf8 update main.py
kaasislekker
2026-08-18 15:23:34 +02:00
ben d2a6e14c5f update main.py
kaasislekker
2026-08-18 15:21:35 +02:00
ben 0cec991cbc update main.py
kaasislekker
2026-08-18 15:18:24 +02:00
ben ab539bb368 update main.py
kaasislekker
2026-08-18 15:15:25 +02:00
ben c39286afc1 updte auth.py
kaasislekker
2026-08-18 15:02:46 +02:00
ben 63b4ee2a6f kaas 2026-08-18 14:43:14 +02:00
ben 3d3270988c kaas 2026-08-18 14:32:33 +02:00
ben 54581e2e42 kaas 2026-08-18 14:27:52 +02:00
ben edada21b4d kaas 2026-08-18 14:06:58 +02:00
ben 405241c1e8 kaas 2026-08-18 14:01:19 +02:00
ben fda883a89a kaas is lekker 2026-08-18 12:00:02 +02:00
ben 9661305d81 fixed small bug 2026-08-18 11:56:16 +02:00
ben f7c906c63b fixed small bug 2026-08-18 11:48:41 +02:00
ben 49e0ea5734 update emails and verifing 2026-08-18 11:44:09 +02:00
ben 953c371e48 update emails and verifing 2026-08-18 11:32:01 +02:00
ben 5f77a4d995 updated some files 2026-08-18 10:50:11 +02:00
ben b85b8dbb25 fixed bugs and small changes 2026-08-18 10:00:25 +02:00
ben a3f252c8d6 fixed bugs and small changes 2026-08-18 09:58:52 +02:00
ben 1f835ba4e8 fixed bugs and small changes 2026-08-18 09:57:36 +02:00
ben 967e139e43 fixed bug 2026-08-17 17:57:15 +02:00
ben 80dc6c37c7 fixed bug 2026-08-17 17:54:08 +02:00
ben c11162fcb3 update time filtering 2026-08-17 17:52:01 +02:00
ben 2c66d6b075 update time filtering 2026-08-17 17:50:36 +02:00
ben e0eb2d5d8f update rooms.py and main.py 2026-08-17 17:41:10 +02:00
ben 139e17114a update rooms.py and main.py 2026-08-17 17:33:23 +02:00
ben 996372c89b update rooms.py and main.py 2026-08-17 17:29:39 +02:00
ben a585a6fa17 update remove_user_as_admin 2026-08-17 17:08:54 +02:00
ben c527a3625e update remove_user_as_admin 2026-08-17 17:04:53 +02:00
ben 4e6d58386f update remove_user_as_admin 2026-08-17 16:57:21 +02:00
ben a8a3ce7399 VERSION 0.2.0 2026-08-17 16:31:35 +02:00
ben 80f7f2978d restructured 2026-08-17 16:29:11 +02:00
ben d47a46c81e restructured 2026-08-17 16:08:13 +02:00
ben 371b6364fe restructured 2026-08-17 15:50:41 +02:00
ben bbc8627867 update endopoints 2026-08-17 15:34:51 +02:00
ben fca7689d49 update endopoints 2026-08-17 15:29:48 +02:00
ben 73bd655898 update endopoints 2026-08-17 15:22:12 +02:00
ben 5a9b820bc6 update endopoints 2026-08-17 15:17:59 +02:00
ben 36a6c47081 update endopoints 2026-08-17 15:15:37 +02:00
ben 80e46c09a3 update endopoints 2026-08-17 14:52:44 +02:00
ben f43c32c72b update server helper 2026-08-17 12:28:54 +02:00
ben 6cc1f71a85 smol 2026-08-17 10:54:41 +02:00
ben 215cc47cc3 kaas 2026-08-17 10:09:39 +02:00
ben 9c97f2dab7 remove curl_request 2026-08-16 18:19:57 +02:00
ben 1adafcbfb0 update frontnd 2026-08-07 18:07:56 +02:00
ben 40ffb103ed update frontend 2026-08-07 17:38:12 +02:00
ben 57757e90aa updates frontend 2026-08-07 10:44:31 +02:00
ben 5102263492 updates 2026-08-04 18:28:01 +02:00
26 changed files with 2983 additions and 2510 deletions
+27 -6
View File
@@ -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
View File
@@ -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",
}
-12
View File
@@ -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
View File
@@ -5,9 +5,6 @@ DB_PATH = Path(__file__).parent.parent / "database" / "calendar.db"
def get_connection(): def get_connection():
"""
Maakt een verbinding met de SQLite database.
"""
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA foreign_keys = ON")
@@ -18,9 +15,6 @@ def get_connection():
def execute(query, params=()): def execute(query, params=()):
"""
Voor INSERT, UPDATE, DELETE.
"""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query, params) cursor.execute(query, params)
@@ -30,9 +24,6 @@ def execute(query, params=()):
def fetch_one(query, params=()): def fetch_one(query, params=()):
"""
Haalt één resultaat op.
"""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query, params) cursor.execute(query, params)
@@ -52,9 +43,6 @@ def fetch_all(query, params=()):
def test_connection(): def test_connection():
"""
Test of de database bereikbaar is.
"""
with get_connection() as conn: with get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
@@ -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'
"""
)
+472
View File
@@ -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()
+12 -1
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+87 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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])
+43
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
-100
View File
@@ -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>
-632
View File
@@ -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
View File
@@ -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 ~
+8
View File
@@ -0,0 +1,8 @@
bcrypt
python-jose
python-dotenv
fastapi
pydantic
uvicorn
email-validator
python-multipart