+import datetime
import json
import secrets
-from fastapi import APIRouter, HTTPException
+from fastapi import APIRouter, HTTPException, Response
from pydantic import BaseModel
from ..utils import fileHandling
from ..utils import security
+from ..utils.settings import settings
import logging
+import base64
+import jwt
logger = logging.getLogger("dailytxtLogger")
password: str
@router.post("/users/login")
-async def login(login: Login):
- print(login.username, login.password)
- return {"message": "Login"}
+async def login(login: Login, respose: Response):
+ # check if user exists
+ content:dict = fileHandling.getUsers()
+ if len(content) == 0 or "users" not in content.keys() or len(content["users"]) == 0 or not any(user["username"] == login.username for user in content["users"]):
+ logger.error(f"Login failed. User '{login.username}' not found")
+ raise HTTPException(status_code=404, detail="User not found")
+
+ # get user data
+ user = next(user for user in content["users"] if user["username"] == login.username)
+ if not security.verify_password(login.password, user["password"]):
+ logger.error(f"Login failed. Password for user '{login.username}' is incorrect")
+ raise HTTPException(status_code=400, detail="Password is incorrect")
+
+ # 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)
+ 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")
+
+
+@router.get("/users/logout")
+def logout(response: Response):
+ response.delete_cookie("jwt")
+ return {"success": True}
class Register(BaseModel):
@router.post("/users/register")
async def register(register: Register):
- content = fileHandling.getUsers()
- if isinstance(content, Exception):
- raise HTTPException(status_code=500, detail="Internal Server Error when trying to open users.json")
-
+ content:dict = fileHandling.getUsers()
# check if username already exists
if len(content) > 0:
username = register.username
password = security.hash_password(register.password)
salt = secrets.token_urlsafe(16)
- enc_enc_key = security.create_new_enc_enc_key(register.password, salt.encode()).decode()
+ enc_enc_key = security.create_new_enc_enc_key(register.password, salt).decode()
if len(content) == 0:
content = {"id_counter": 1, "users": [
{
"user_id": 1,
- "dailytxt-version": 2,
+ "dailytxt_version": 2,
"username": username,
"password": password,
"salt": salt,
content["users"].append(
{
"user_id": content["id_counter"],
- "dailytxt-version": 2,
+ "dailytxt_version": 2,
"username": username,
"password": password,
"salt": salt,
import json
import os
import logging
+
+from fastapi import HTTPException
from .settings import settings
logger = logging.getLogger("dailytxtLogger")
f = open(os.path.join(settings.data_path, "users.json"), "r")
except FileNotFoundError:
logger.info("users.json - File not found")
- return ""
+ return {}
except Exception as e:
logger.exception(e)
- return e
+ raise HTTPException(status_code=500, detail="Internal Server Error when trying to open users.json")
else:
with f:
- return f.read()
+ s = f.read()
+ if s == "":
+ return {}
+ return json.loads(s)
def writeUsers(content):
# print working directory
def verify_password(password: str, hash: str) -> bool:
return argon2.verify(password, hash)
-def derive_key_from_password(password: str, salt: bytes) -> bytes:
- return hash_secret_raw(secret=password.encode(), salt=salt, time_cost=2, memory_cost=2**15, parallelism=1, hash_len=32, type=Type.ID)
+def derive_key_from_password(password: str, salt: str) -> bytes:
+ return hash_secret_raw(secret=password.encode(), salt=salt.encode(), time_cost=2, memory_cost=2**15, parallelism=1, hash_len=32, type=Type.ID)
-def create_new_enc_enc_key(password: str, salt: bytes) -> bytes:
+def create_new_enc_enc_key(password: str, salt: str) -> bytes:
derived_key = derive_key_from_password(password, salt) # password derived key only to encrypt the actual encryption key
key = Fernet.generate_key() # actual encryption key
f = Fernet(base64.urlsafe_b64encode(derived_key))
return f.encrypt(key)
-
-
\ No newline at end of file
+import secrets
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
data_path: str = "/data"
+ secret_token: str = secrets.token_urlsafe(32)
+ logout_after_days: int = 30
settings = Settings()
\ No newline at end of file
<script>
import { fade, blur, slide } from 'svelte/transition';
+ import axios from 'axios';
+ import { dev } from '$app/environment';
+ import { goto } from '$app/navigation';
+ import { onMount } from 'svelte';
export let data;
let inDuration = 150;
let outDuration = 150;
+
+ let API_URL = dev ? 'http://localhost:8000' : window.location.pathname.replace(/\/+$/, '');
+
+ function logout() {
+ axios
+ .get(API_URL + '/users/logout')
+ .then((response) => {
+ localStorage.removeItem('user');
+ goto('/login');
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ }
</script>
<main class="d-flex flex-column">
<div class="container-fluid">
<a class="nav-item" href="/">Navbar</a>
<a class="nav-item" href="/login">Navbar</a>
+ <div class="dropdown">
+ <button
+ class="btn btn-outline-secondary dropdown-toggle"
+ type="button"
+ data-bs-toggle="dropdown"
+ aria-expanded="false"
+ >
+ Dropdown button
+ </button>
+ <ul class="dropdown-menu dropdown-menu-end">
+ <li><button class="dropdown-item">Settings</button></li>
+ <li><button class="dropdown-item" onclick={logout}>Logout</button></li>
+ </ul>
+ </div>
</div>
</nav>
--- /dev/null
+import {redirect} from '@sveltejs/kit'
+
+export const load = () => {
+ const user = JSON.parse(localStorage.getItem('user'));
+ if (!user) {
+ throw redirect(307, '/login');
+ }
+}
\ No newline at end of file
import '../scss/styles.scss';
//import * as bootstrap from 'bootstrap';
import { Tooltip } from 'bootstrap';
+ import { goto } from '$app/navigation';
//on mount
import { onMount } from 'svelte';
--- /dev/null
+import {redirect} from '@sveltejs/kit'
+
+export const load = () => {
+ const user = JSON.parse(localStorage.getItem('user'));
+ if (user) {
+ throw redirect(307, '/');
+ }
+}
\ No newline at end of file
import { onMount } from 'svelte';
import axios from 'axios';
import { dev } from '$app/environment';
+ import { goto } from '$app/navigation';
let show_warning_empty_fields = $state(false);
let show_warning_passwords_do_not_match = $state(false);
axios
.post(API_URL + '/users/login', { username, password })
.then((response) => {
- console.log(response);
+ localStorage.setItem('user', JSON.stringify(response.data.username));
+ goto('/');
})
.catch((error) => {
console.error(error);