writing logs is working
authorPhiTux <redacted>
Sat, 28 Dec 2024 17:46:04 +0000 (18:46 +0100)
committerPhiTux <redacted>
Sat, 28 Dec 2024 17:46:04 +0000 (18:46 +0100)
backend/server/main.py
backend/server/routers/logs.py [new file with mode: 0644]
backend/server/routers/users.py
backend/server/utils/fileHandling.py
backend/server/utils/security.py
backend/server/utils/settings.py
frontend/src/routes/+page.svelte
frontend/src/routes/Datepicker.svelte
frontend/src/routes/login/+page.svelte

index 2a933c9e9d81395ddfa3909d16244e68f19bc09c..c358ad34af19ba5dc74db39d17012971165802e0 100644 (file)
@@ -1,5 +1,5 @@
 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
@@ -8,11 +8,13 @@ logger = logging.getLogger("dailytxtLogger")
 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(
@@ -23,7 +25,8 @@ 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():
diff --git a/backend/server/routers/logs.py b/backend/server/routers/logs.py
new file mode 100644 (file)
index 0000000..b4e161b
--- /dev/null
@@ -0,0 +1,88 @@
+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
index 07be90df532558e4092e2a8858aa318bff0563b4..ef825ea495b24f27144be4165be9829456b06d3b 100644 (file)
@@ -2,7 +2,7 @@ import asyncio
 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
@@ -19,8 +19,8 @@ class Login(BaseModel):
     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()
@@ -37,19 +37,30 @@ async def login(login: Login, respose: Response):
     # 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}
 
 
@@ -57,7 +68,7 @@ class Register(BaseModel):
     username: str
     password: str
 
-@router.post("/users/register")
+@router.post("/register")
 async def register(register: Register):
     content:dict = fileHandling.getUsers()
 
index b1b0121c8266b9f9053f8838c52838a16cb2a2c9..fb78d71881f8e57e1268517051043cbf87c57a2f 100644 (file)
@@ -23,6 +23,22 @@ def 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:
@@ -30,6 +46,18 @@ def writeUsers(content):
     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))
index cf5af99eeb1bee41311f7c1ef966c23b04e3608d..69602add7499f2f8e2384e25fb41f805167dd13c 100644 (file)
@@ -17,3 +17,11 @@ def create_new_enc_enc_key(password: str, salt: str) -> bytes:
     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
index 17811860fe3dd5c7e13221295303973b8bf3db41..b1e1fc9f145a7492906043a5487f871f75755f0b 100644 (file)
@@ -3,7 +3,8 @@ from pydantic_settings import BaseSettings
 
 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
index c63f5ee70c8b4f7c2e6690e6bb94a367aa0b156e..512b8f7b5001df5b59a749f7d1644f7ae23413bd 100644 (file)
@@ -3,17 +3,53 @@
        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>
index 3d1da39f76d197fa3be5691d22d904a50883a7b2..9274d056f404059c3d247ab99aa0e59dab64e9ce 100644 (file)
@@ -40,7 +40,7 @@
                        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 -->
index 0a3f460bcaa6eef5ffc1ac1e9524e88e5b040d98..c5af14156496302c84cdca8d34c9012a8b40695e 100644 (file)
@@ -1,9 +1,11 @@
 <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>
git clone https://git.99rst.org/PROJECT