from fastapi import FastAPI
-from .routers import users
+from .routers import users, logs
from fastapi.middleware.cors import CORSMiddleware
import logging
from sys import stdout
consoleHandler = logging.StreamHandler(stdout)
consoleHandler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(consoleHandler)
+logger.setLevel(logging.DEBUG)
app = FastAPI()
origins = [
"http://localhost:5173",
+ "localhost:5173",
]
app.add_middleware(
allow_headers=["*"],
)
-app.include_router(users.router)
+app.include_router(users.router, prefix="/users")
+app.include_router(logs.router, prefix="/logs")
@app.get("/")
async def root():
--- /dev/null
+import datetime
+import logging
+from fastapi import APIRouter, Cookie
+from pydantic import BaseModel
+from fastapi import Depends
+from . import users
+from ..utils import fileHandling
+from ..utils import security
+
+
+logger = logging.getLogger("dailytxtLogger")
+
+router = APIRouter()
+
+
+class Log(BaseModel):
+ date: str
+ text: str
+ date_written: str
+
+@router.post("/saveLog")
+async def saveLog(log: Log, cookie = Depends(users.isLoggedIn)):
+ print(datetime.datetime.fromisoformat(log.date))
+ year = datetime.datetime.fromisoformat(log.date).year
+ month = datetime.datetime.fromisoformat(log.date).month
+ day = datetime.datetime.fromisoformat(log.date).day
+
+ content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+
+ # move old log to history
+ if "days" in content.keys():
+ for dayLog in content["days"]:
+ if dayLog["day"] == day:
+ historyVersion = 0
+ if "history" not in dayLog.keys():
+ dayLog["history"] = []
+ else:
+ for historyLog in dayLog["history"]:
+ if historyLog["version"] > historyVersion:
+ historyVersion = historyLog["version"]
+ historyVersion += 1
+ dayLog["history"].append({"version": historyVersion, "text": dayLog["text"], "date_written": dayLog["date_written"]})
+ break
+
+ # save new log
+ encrypted_text = security.encrypt_text(log.text, cookie["derived_key"])
+ encrypted_date_written = security.encrypt_text(log.date_written, cookie["derived_key"])
+
+ if "days" not in content.keys():
+ content["days"] = []
+ content["days"].append({"day": day, "text": encrypted_text, "date_written": encrypted_date_written})
+ else:
+ found = False
+ for dayLog in content["days"]:
+ if dayLog["day"] == day:
+ dayLog["text"] = encrypted_text
+ dayLog["date_written"] = encrypted_date_written
+ found = True
+ break
+ if not found:
+ content["days"].append({"day": day, "text": encrypted_text, "date_written": encrypted_date_written})
+
+ if not fileHandling.writeDay(cookie["user_id"], year, month, content):
+ logger.error(f"Failed to save log for {cookie['user_id']} on {year}-{month:02d}-{day:02d}")
+ return {"success": False}
+
+ return {"success": True}
+
+
+@router.get("/getLog")
+async def getLog(date: str, cookie = Depends(users.isLoggedIn)):
+
+ year = datetime.datetime.fromisoformat(date).year
+ month = datetime.datetime.fromisoformat(date).month
+ day = datetime.datetime.fromisoformat(date).day
+
+ content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+
+ if "days" not in content.keys():
+ return {"text": "", "date_written": ""}
+
+ for dayLog in content["days"]:
+ if dayLog["day"] == day:
+ text = security.decrypt_text(dayLog["text"], cookie["derived_key"])
+ date_written = security.decrypt_text(dayLog["date_written"], cookie["derived_key"])
+ return {"text": text, "date_written": date_written}
+
+ return {"text": "", "date_written": ""}
\ No newline at end of file
import datetime
import json
import secrets
-from fastapi import APIRouter, HTTPException, Response
+from fastapi import APIRouter, Cookie, HTTPException, Response
from pydantic import BaseModel
from ..utils import fileHandling
from ..utils import security
username: str
password: str
-@router.post("/users/login")
-async def login(login: Login, respose: Response):
+@router.post("/login")
+async def login(login: Login, response: Response):
# check if user exists
content:dict = fileHandling.getUsers()
# get intermediate key
derived_key = base64.b64encode(security.derive_key_from_password(login.password, user["salt"])).decode()
-
# build jwt
- jwt = create_jwt(user["user_id"], user["username"], derived_key)
- respose.set_cookie(key="jwt", value=jwt, httponly=True)
+ token = create_jwt(user["user_id"], user["username"], derived_key)
+ response.set_cookie(key="token", value=token, httponly=True)
return {"username": user["username"]}
def create_jwt(user_id, username, derived_key):
- return jwt.encode({"iat": datetime.datetime.now() + datetime.timedelta(days=settings.logout_after_days), "user_id": user_id, "name": username, "derived_key": derived_key}, settings.secret_token, algorithm="HS256")
+ return jwt.encode({"exp": datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=settings.logout_after_days), "user_id": user_id, "name": username, "derived_key": derived_key}, settings.secret_token, algorithm="HS256")
+
+def decode_jwt(token):
+ return jwt.decode(token, settings.secret_token, algorithms="HS256")
+
+def isLoggedIn(token: str = Cookie()) -> int:
+ try:
+ decoded = decode_jwt(token)
+ return decoded
+ except jwt.ExpiredSignatureError:
+ raise HTTPException(status_code=440, detail="Token expired")
+ except:
+ raise HTTPException(status_code=401, detail="Not logged in")
-@router.get("/users/logout")
+@router.get("/logout")
def logout(response: Response):
- response.delete_cookie("jwt")
+ response.delete_cookie("token", httponly=True)
return {"success": True}
username: str
password: str
-@router.post("/users/register")
+@router.post("/register")
async def register(register: Register):
content:dict = fileHandling.getUsers()
return {}
return json.loads(s)
+def getDay(user_id, year, month):
+ try:
+ f = open(os.path.join(settings.data_path, f"{user_id}/{year}/{month:02d}.json"), "r")
+ except FileNotFoundError:
+ logger.info(f"{user_id}/{year}/{month:02d}.json - File not found")
+ return {}
+ except Exception as e:
+ logger.exception(e)
+ raise HTTPException(status_code=500, detail="Internal Server Error when trying to open {year}-{month}.json")
+ else:
+ with f:
+ s = f.read()
+ if s == "":
+ return {}
+ return json.loads(s)
+
def writeUsers(content):
# print working directory
try:
except Exception as e:
logger.exception(e)
return e
+ else:
+ with f:
+ f.write(json.dumps(content, indent=4))
+ return True
+
+def writeDay(user_id, year, month, content):
+ try:
+ os.makedirs(os.path.join(settings.data_path, f"{user_id}/{year}"), exist_ok=True)
+ f = open(os.path.join(settings.data_path, f"{user_id}/{year}/{month:02d}.json"), "w")
+ except Exception as e:
+ logger.exception(e)
+ return False
else:
with f:
f.write(json.dumps(content, indent=4))
key = Fernet.generate_key() # actual encryption key
f = Fernet(base64.urlsafe_b64encode(derived_key))
return f.encrypt(key)
+
+def encrypt_text(text: str, derived_key: str) -> str:
+ f = Fernet(base64.urlsafe_b64encode(base64.b64decode(derived_key)))
+ return f.encrypt(text.encode()).decode()
+
+def decrypt_text(text: str, derived_key: str) -> str:
+ f = Fernet(base64.urlsafe_b64encode(base64.b64decode(derived_key)))
+ return f.decrypt(text.encode()).decode()
\ No newline at end of file
class Settings(BaseSettings):
data_path: str = "/data"
- secret_token: str = secrets.token_urlsafe(32)
+ development: bool = False
+ secret_token: str = secrets.token_urlsafe(32)
logout_after_days: int = 30
settings = Settings()
\ No newline at end of file
import * as bootstrap from 'bootstrap';
import Sidenav from './Sidenav.svelte';
import { selectedDate } from '$lib/calendarStore.js';
- import dayjs from 'dayjs';
+ import axios from 'axios';
+ import { dev } from '$app/environment';
+ import { goto } from '$app/navigation';
+
+ let API_URL = dev ? 'http://localhost:8000' : window.location.pathname.replace(/\/+$/, '');
+
+ axios.interceptors.request.use((config) => {
+ config.withCredentials = true;
+ return config;
+ });
+
+ axios.interceptors.response.use(
+ (response) => {
+ return response;
+ },
+ (error) => {
+ if (
+ error.response &&
+ error.response.status &&
+ (error.response.status == 401 || error.response.status == 440)
+ ) {
+ // logout
+ axios
+ .get(API_URL + '/users/logout')
+ .then((response) => {
+ localStorage.removeItem('user');
+ goto(`/login?error=${error.response.status}`);
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ }
+ return Promise.reject(error);
+ }
+ );
$effect(() => {
if ($selectedDate) {
- console.log('hu');
+ console.log('selectedDate changed');
}
});
let currentLog = $state('');
let savedLog = $state('');
+ let logDateWritten = $state('');
+
let timeout;
function debounce(fn) {
function saveLog() {
// axios to backend
- console.log(dayjs().format('DD.MM.YYYY, HH:mm [Uhr]'));
- savedLog = currentLog;
+ let date_written = new Date().toLocaleString('de-DE', {
+ timeZone: 'Europe/Berlin',
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+
+ console.log(new Date($selectedDate).toISOString());
+
+ axios
+ .post(API_URL + '/logs/saveLog', {
+ date: new Date($selectedDate).toISOString(),
+ text: currentLog,
+ date_written: date_written
+ })
+ .then((response) => {
+ if (response.data.success) {
+ savedLog = currentLog;
+ logDateWritten = date_written;
+ } else {
+ // toast
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorSavingLog'));
+ toast.show();
+ console.error('Log not saved');
+ }
+ })
+ .catch((error) => {
+ // toast
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorSavingLog'));
+ toast.show();
+ console.error(error.response);
+ });
}
</script>
</div>
<div class="flex-fill textAreaWrittenAt">
Geschrieben am:<br />
- TODO
+ {logDateWritten}
</div>
<div class="textAreaHistory">history</div>
<div class="textAreaDelete">delete</div>
</div>
<div id="right">Right</div>
+
+ <div class="toast-container position-fixed bottom-0 end-0 p-3">
+ <div
+ id="toastErrorSavingLog"
+ class="toast align-items-center text-bg-danger"
+ role="alert"
+ aria-live="assertive"
+ aria-atomic="true"
+ >
+ <div class="d-flex">
+ <div class="toast-body">Fehler beim Speichern des Textes!</div>
+ </div>
+ </div>
+ </div>
</div>
<style>
const dayKey = `${year}-${(month + 1).toString().padStart(2, '0')}-${i
.toString()
.padStart(2, '0')}`;
- tempDays.push({ date: new Date(year, month, i), mark: markedDays[dayKey] });
+ tempDays.push({ date: new Date(Date.UTC(year, month, i)), mark: markedDays[dayKey] });
}
return tempDays;
{#each weekDays as day}
<div class="day-header">{day}</div>
{/each}
- {#each days as day (day ? day.date : Math.random())}
+ {#each days as day}
{#if day}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<script>
import img from '$lib/assets/locked_heart_with_keyhole.svg';
+ import * as bootstrap from 'bootstrap';
import { onMount } from 'svelte';
import axios from 'axios';
import { dev } from '$app/environment';
import { goto } from '$app/navigation';
+ import { page } from '$app/stores';
let show_login_failed = $state(false);
let show_login_warning_empty_fields = $state(false);
let API_URL = dev ? 'http://localhost:8000' : window.location.pathname.replace(/\/+$/, '');
+ onMount(() => {
+ // if params error=440 or error=401, show toast
+ if (window.location.search.includes('error=440')) {
+ const toast = new bootstrap.Toast(document.getElementById('toastLoginExpired'));
+ toast.show();
+ } else if (window.location.search.includes('error=401')) {
+ const toast = new bootstrap.Toast(document.getElementById('toastLoginInvalid'));
+ toast.show();
+ }
+ });
+
function handleLogin(event) {
event.preventDefault();
</div>
</div>
</div>
+
+ <div class="toast-container position-fixed bottom-0 end-0 p-3">
+ <div
+ id="toastLoginExpired"
+ class="toast align-items-center text-bg-danger"
+ role="alert"
+ aria-live="assertive"
+ aria-atomic="true"
+ >
+ <div class="d-flex">
+ <div class="toast-body">Der Login ist abgelaufen. Bitte neu anmelden.</div>
+ </div>
+ </div>
+
+ <div
+ id="toastLoginInvalid"
+ class="toast align-items-center text-bg-danger"
+ role="alert"
+ aria-live="assertive"
+ aria-atomic="true"
+ >
+ <div class="d-flex">
+ <div class="toast-body">Authentifizierung fehlgeschlagen. Bitte neu anmelden.</div>
+ </div>
+ </div>
+ </div>
</div>
<style>