implement basic login

This commit is contained in:
Anika Raemer 2025-10-07 19:27:35 +02:00
parent bdd90b50d9
commit 7a6f5b5bcd
18 changed files with 222 additions and 35 deletions

View file

@ -2,9 +2,10 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
import RecipeDetailPage from "./components/recipes/RecipeDetailPage"
import RecipeEditPage from "./components/recipes/RecipeEditPage"
import RecipeListPage from "./components/recipes/RecipeListPage"
import { getRecipeAddUrlDefinition, getRecipeDetailsUrlDefinition, getRecipeEditUrlDefinition, getRootUrlDefinition } from "./routes"
import { getLoginUrl, getRecipeAddUrlDefinition, getRecipeDetailsUrlDefinition, getRecipeEditUrlDefinition, getRecipeListUrlDefinition, getRootUrlDefinition } from "./routes"
import "./App.css"
import LoginPage from "./components/LoginPage"
/**
* Main application component.
@ -14,8 +15,10 @@ function App() {
return (
<Router>
<Routes>
{/* Login page */}
<Route path={getLoginUrl()} element={<LoginPage/>}/>
{/* Home page: list of recipes */}
<Route path= {getRootUrlDefinition()} element={<RecipeListPage />} />
<Route path= {getRecipeListUrlDefinition()} element={<RecipeListPage />} />
{/* Detail page: shows one recipe */}
<Route path={getRecipeDetailsUrlDefinition()} element={<RecipeDetailPage />} />

View file

@ -0,0 +1,7 @@
/**
* Defines a login request
*/
export class LoginRequestDto {
userName?: string;
password?: string;
}

View file

@ -0,0 +1,10 @@
import { UserDto } from "./UserDto.js";
/**
* Response to a successful login
*/
export class LoginResponseDto {
userData?: UserDto;
token?: string;
expiryDate? : Date;
}

View file

@ -0,0 +1,15 @@
import { AbstractDto } from "./AbstractDto.js";
import { RecipeIngredientGroupDto } from "./RecipeIngredientGroupDto.js";
import { RecipeInstructionStepDto } from "./RecipeInstructionStepDto.js";
/**
* DTO describing a recipe
*/
export class RecipeDto extends AbstractDto {
title!: string;
amount?: number
amountDescription?: string;
instructions!: RecipeInstructionStepDto[];
ingredientGroups!: RecipeIngredientGroupDto[];
}

View file

@ -0,0 +1,10 @@
import { AbstractDto } from "./AbstractDto.js";
export class RecipeIngredientDto extends AbstractDto{
name!: string;
subtext?: string;
amount?: number;
unit?: string;
sortOrder!: number;
ingredientGroupId?: string;
}

View file

@ -0,0 +1,9 @@
import { AbstractDto } from "./AbstractDto.js";
import { RecipeIngredientDto } from "./RecipeIngredientDto.js";
export class RecipeIngredientGroupDto extends AbstractDto{
title?: string;
sortOrder!: number;
recipeId?: string;
ingredients!: RecipeIngredientDto[];
}

View file

@ -0,0 +1,8 @@
import { UUID } from "crypto";
import { AbstractDto } from "./AbstractDto.js";
export class RecipeInstructionStepDto extends AbstractDto{
text!: string;
sortOrder!: number;
recipeId?: UUID;
}

View file

@ -0,0 +1,9 @@
import { AbstractDto } from "./AbstractDto.js";
export class UserDto extends AbstractDto {
firstName?: string;
lastName?: string;
userName!: string;
email!: string;
role?: string;
}

View file

@ -0,0 +1,27 @@
import type { Recipe } from "../../types/recipe"
import type { LoginRequestDto } from "../dtos/LoginRequestDto";
import type { LoginResponseDto } from "../dtos/LoginResponseDto";
import { get, postJson, putJson } from "../utils/requests";
/**
* Util for handling the recipe api
*/
// read base url from .env file
const BASE_URL = import.meta.env.VITE_API_BASE;
/**
* URL for handling recipes
*/
const AUTH_URL = `${BASE_URL}/auth`
/**
* Create new Recipe
* @param recipe Recipe to create
* @returns Saved recipe
*/
export async function login(requestDto: LoginRequestDto): Promise<LoginResponseDto> {
const res = await postJson(`${AUTH_URL}/login`, JSON.stringify(requestDto), false);
return res.json();
}

View file

@ -0,0 +1,29 @@
import type { Recipe } from "../../types/recipe"
import { get, postJson, putJson } from "../utils/requests";
/**
* Util for handling the recipe api
*/
// read base url from .env file
const BASE_URL = import.meta.env.VITE_API_BASE;
/**
* URL for handling recipes header data
*/
const RECIPE_URL = `${BASE_URL}/compact-recipe`
/**
* Load list of all recipes
* @param searchString Search string for filtering recipeList
* @returns Array of recipe
*/
export async function fetchRecipeList(searchString : string): Promise<Recipe[]> {
let url : string = RECIPE_URL;
// if there's a search string add it as query parameter
if(searchString && searchString !== ""){
url +="?search=" + searchString;
}
const res = await get(url);
return res.json();
}

View file

@ -1,5 +1,5 @@
import type { Recipe } from "../types/recipe"
import { get, postJson, putJson } from "./utils/requests";
import type { Recipe } from "../../types/recipe"
import { get, postJson, putJson } from "../utils/requests";
/**
@ -23,21 +23,6 @@ export async function fetchRecipe(id: string): Promise<Recipe> {
return res.json()
}
/**
* Load list of all recipes
* @param searchString Search string for filtering recipeList
* @returns Array of recipe
*/
export async function fetchRecipeList(searchString : string): Promise<Recipe[]> {
let url : string = RECIPE_URL;
// if there's a search string add it as query parameter
if(searchString && searchString !== ""){
url +="?search=" + searchString;
}
const res = await get(url);
return res.json();
}
/**
* Create new Recipe
* @param recipe Recipe to create

View file

@ -1,3 +1,12 @@
const BASE_URL = import.meta.env.VITE_API_BASE;
export function createBasicHeader() : Headers {
const headers = new Headers();
//headers.set('Access-Control-Allow-Origin', '*');
return headers;
}
export function setContentTypeHeaderJson(headers: Headers){
return headers.set("Content-Type", "application/json");
}

View file

@ -1,7 +1,7 @@
import { setAuthHeader, setContentTypeHeaderJson } from "./headers";
import { createBasicHeader, setAuthHeader, setContentTypeHeaderJson } from "./headers";
export async function get(url: string) : Promise<Response>{
const requestHeaders = new Headers();
const requestHeaders = createBasicHeader();
setAuthHeader(requestHeaders);
console.log("GET to " + url);
const response = await fetch(url, {
@ -15,24 +15,33 @@ export async function get(url: string) : Promise<Response>{
}
export async function postJson(url: string, requestBody: string, logBody = true) : Promise<Response>{
console.log("POST to " + url + (logBody) ? "with body " + requestBody : "");
return persistJson(url, requestBody, "POST");
console.log("POST to " + url);
if(logBody){
console.log("body: " + requestBody);
}
return await persistJson(url, requestBody, "POST");
}
export async function putJson(url: string, requestBody: string, logBody = true) : Promise<Response>{
console.log("PUT to " + url + (logBody) ? "with body " + requestBody : "");
console.log("PUT to " + url);
if(logBody){
console.log("body: " + requestBody);
}
return persistJson(url, requestBody, "PUT");
}
async function persistJson(url: string, requestBody: string, requestMethod: string) : Promise<Response>{
const requestHeaders = new Headers();
const requestHeaders = createBasicHeader();
setContentTypeHeaderJson(requestHeaders);
setAuthHeader(requestHeaders);
const response = await fetch(url, {
const request = new Request(url, {
method: requestMethod,
headers: requestHeaders,
body: requestBody,
});
console.log(request)
const response = await fetch(request);
console.log("response.ok", response.ok)
if(!response.ok){
throw new Error(requestMethod + " to " + url + " failed!")
}

View file

@ -0,0 +1,55 @@
import { useState } from "react";
import Button from "./basics/Button";
import type { LoginRequestDto } from "../api/dtos/LoginRequestDto";
import type { LoginResponseDto } from "../api/dtos/LoginResponseDto";
import { login } from "../api/points/AuthPoint";
import { getRecipeListUrl } from "../routes";
import { useNavigate } from "react-router-dom";
export default function LoginPage(){
const [userName, setUserName] = useState<string>("");
const [password, setPassword] = useState<string>("");
const navigate = useNavigate();
const executeLogin = async () =>{
const dto : LoginRequestDto = {
userName: userName,
password: password
}
try{
// @todo move to auth handler
console.log("trying to log in with " + dto.userName)
const loginResponse : LoginResponseDto = await login(dto);
localStorage.setItem("session", JSON.stringify(loginResponse));
console.log("Successfully logged on with user " + loginResponse.userData?.userName)
navigate(getRecipeListUrl());
} catch(err){
// @todo show error in GUI
console.error(err);
}
}
return(
<div className="p-6 max-w-2xl mx-auto">
<input
className="input-field"
placeholder="Benutzername"
value = {userName}
onChange={e => {
setUserName(e.target.value)
}}
/>
{/* @todo Password mode!!! */}
<input
className="input-field"
placeholder="Passwort"
value = {password}
onChange={e => {
setPassword(e.target.value)
}}
/>
<Button
text="Login"
onClick= {executeLogin}
/>
</div>
);
}

View file

@ -1,7 +1,7 @@
import { useParams, Link } from "react-router-dom"
import { useParams } from "react-router-dom"
import type { Recipe } from "../../types/recipe"
import { useEffect, useState } from "react"
import { fetchRecipe } from "../../api/recipePoint"
import { fetchRecipe } from "../../api/points/RecipePoint"
import { getRecipeEditUrl, getRecipeListUrl } from "../../routes"
import ButtonLink from "../basics/ButtonLink"

View file

@ -2,8 +2,8 @@ import { useParams, useNavigate } from "react-router-dom"
import { useEffect, useState } from "react"
import type { Recipe } from "../../types/recipe"
import RecipeEditor from "./RecipeEditor"
import { fetchRecipe, createRecipe, updateRecipe } from "../../api/recipePoint"
import { getRecipeDetailUrl, getRecipeListUrl, getRootUrl } from "../../routes"
import { fetchRecipe, createRecipe, updateRecipe } from "../../api/points/RecipePoint"
import { getRecipeDetailUrl, getRecipeListUrl } from "../../routes"
export default function RecipeEditPage() {
// Extract recipe ID from route params

View file

@ -1,10 +1,9 @@
import { useEffect, useState } from "react"
import RecipeListItem from "./RecipeListItem"
import type { Recipe } from "../../types/recipe"
import { fetchRecipeList } from "../../api/recipePoint"
import { fetchRecipeList } from "../../api/points/CompactRecipePoint"
import { useNavigate } from "react-router-dom"
import { getRecipeAddUrl, getRecipeDetailUrl } from "../../routes"
import SearchField from "../basics/SearchField"
import RecipeListToolbar from "./RecipeListToolbar"
/**

View file

@ -6,10 +6,13 @@ export function getRootUrlDefinition() : string { return getRootUrl()}
export function getRecipeDetailsUrlDefinition() : string {return getRecipeDetailUrl(":id")}
export function getRecipeEditUrlDefinition() : string {return getRecipeEditUrl(":id")}
export function getRecipeAddUrlDefinition() : string {return getRecipeAddUrl()}
export function getRecipeListUrlDefinition() : string {return getRecipeListUrl()}
export function getLoginUrlDefinition() : string {return getLoginUrl()}
// URLs including id
export function getRootUrl () : string { return "/"}
export function getRecipeListUrl() : string {return getRootUrl()}
export function getRecipeListUrl() : string {return "/recipe/list"}
export function getRecipeDetailUrl(id: string) : string {return "/recipe/" + id + "/card"}
export function getRecipeEditUrl(id: string) : string {return "/recipe/" + id + "/edit"}
export function getRecipeAddUrl() : string {return "/recipe/add"}
export function getLoginUrl() : string {return "/login"}