diff --git a/00-backend/datasource/database.sqlite b/00-backend/datasource/database.sqlite index 1e00484..246d0e0 100644 Binary files a/00-backend/datasource/database.sqlite and b/00-backend/datasource/database.sqlite differ diff --git a/00-backend/src/main/java/de/htwsaar/webshop/cronjob/ExpiredSessionDeleteJob.java b/00-backend/src/main/java/de/htwsaar/webshop/cronjob/ExpiredSessionDeleteJob.java new file mode 100644 index 0000000..8a00b40 --- /dev/null +++ b/00-backend/src/main/java/de/htwsaar/webshop/cronjob/ExpiredSessionDeleteJob.java @@ -0,0 +1,36 @@ +package de.htwsaar.webshop.cronjob; + +import de.htwsaar.webshop.service.SessionService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import static de.htwsaar.webshop.util.TimeUtil.MILLIS_TO_WEEK; + +/** + * CronJob for deleting expired sessions in fixed intervals + */ +@Component +@Slf4j +public class ExpiredSessionDeleteJob { + + private final SessionService sessionService; + + /** + * Constructor of the class + * @param sessionService used in the class + */ + public ExpiredSessionDeleteJob(SessionService sessionService) { + this.sessionService = sessionService; + } + + /** + * Method running the job calling the referring service method + */ + @Scheduled(fixedRate = MILLIS_TO_WEEK) + public void runFixedRateTask() { + log.info("Deleting expired sessions..."); + sessionService.deleteExpired(); + log.info("Deleted expired sessions."); + } +} \ No newline at end of file diff --git a/00-backend/src/main/java/de/htwsaar/webshop/repository/SessionRepository.java b/00-backend/src/main/java/de/htwsaar/webshop/repository/SessionRepository.java index d200c65..6d5f2ca 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/repository/SessionRepository.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/repository/SessionRepository.java @@ -13,4 +13,6 @@ public interface SessionRepository extends JpaRepository { Session findByAccount(Account account); Session getSessionByToken(UUID token); + + void deleteSessionsByTimeoutBefore(Long timeoutBefore); } diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/SessionService.java b/00-backend/src/main/java/de/htwsaar/webshop/service/SessionService.java index 71fe2ae..169b09d 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/service/SessionService.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/service/SessionService.java @@ -19,4 +19,6 @@ public interface SessionService { boolean isValid(UUID token, String email); boolean isAdmin(UUID token, String email); + + void deleteExpired(); } diff --git a/00-backend/src/main/java/de/htwsaar/webshop/service/impl/SessionServiceImpl.java b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/SessionServiceImpl.java index 2f6a6a9..e718d08 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/service/impl/SessionServiceImpl.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/service/impl/SessionServiceImpl.java @@ -57,7 +57,7 @@ public class SessionServiceImpl implements SessionService { if (!session.getAccount().equals(accountEmail)) { return false; } - if (session.getTimeout() >= System.currentTimeMillis()) { + if (session.getTimeout() <= System.currentTimeMillis()) { log.info("Session with email {} is expired", email); delete(session); return false; @@ -92,4 +92,9 @@ public class SessionServiceImpl implements SessionService { log.info("Session with email {} allowed to access Admin Services", email); return true; } + + @Override + public void deleteExpired() { + sessionRepository.deleteSessionsByTimeoutBefore(System.currentTimeMillis()); + } } diff --git a/00-backend/src/main/java/de/htwsaar/webshop/util/TimeUtil.java b/00-backend/src/main/java/de/htwsaar/webshop/util/TimeUtil.java index f9fd439..0cc9549 100644 --- a/00-backend/src/main/java/de/htwsaar/webshop/util/TimeUtil.java +++ b/00-backend/src/main/java/de/htwsaar/webshop/util/TimeUtil.java @@ -46,7 +46,7 @@ public class TimeUtil { } public static long nowMonthsAgo(long months) { - return LocalDateTime.now().minusMonths(months).atZone(ZoneId.systemDefault()).toEpochSecond(); + return LocalDateTime.now().minusMonths(months).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); } public static long monthLength(long epoch) { diff --git a/01-frontend/public/locales/de/translation.json b/01-frontend/public/locales/de/translation.json index 92ff8fe..4332fc3 100644 --- a/01-frontend/public/locales/de/translation.json +++ b/01-frontend/public/locales/de/translation.json @@ -107,5 +107,6 @@ "discount100": "Rabatt in %", "deleteProduct": "Produkt löschen", "description": "Beschreibung", - "images": "Bilder" + "images": "Bilder", + "loggedInAs": "Angemeldet als" } diff --git a/01-frontend/public/locales/en/translation.json b/01-frontend/public/locales/en/translation.json index 83d0cad..faa6f6a 100644 --- a/01-frontend/public/locales/en/translation.json +++ b/01-frontend/public/locales/en/translation.json @@ -107,5 +107,6 @@ "discount100": "Discount in %", "deleteProduct": "Delete Product", "description": "Description", - "images": "Images" + "images": "Images", + "loggedInAs": "Logged in as" } \ No newline at end of file diff --git a/01-frontend/src/helper/AccountProvider.tsx b/01-frontend/src/helper/AccountProvider.tsx index 2f21d00..73ab17c 100644 --- a/01-frontend/src/helper/AccountProvider.tsx +++ b/01-frontend/src/helper/AccountProvider.tsx @@ -21,6 +21,7 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => { ); }; +// eslint-disable-next-line react-refresh/only-export-components export const useAccount = () => { const context = useContext(AccountContext); if (!context) { diff --git a/01-frontend/src/helper/BasketProvider.tsx b/01-frontend/src/helper/BasketProvider.tsx index 32e8674..1bd52b7 100644 --- a/01-frontend/src/helper/BasketProvider.tsx +++ b/01-frontend/src/helper/BasketProvider.tsx @@ -44,6 +44,7 @@ export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({ childr ); }; +// eslint-disable-next-line react-refresh/only-export-components export const useBasket = () => { const context = useContext(BasketContext); if (!context) { diff --git a/01-frontend/src/helper/adminpanel/ItemsInfo.tsx b/01-frontend/src/helper/adminpanel/ItemsInfo.tsx index d5f8d3d..8da52b7 100644 --- a/01-frontend/src/helper/adminpanel/ItemsInfo.tsx +++ b/01-frontend/src/helper/adminpanel/ItemsInfo.tsx @@ -4,8 +4,13 @@ import {Box, Button, IconButton, Toolbar, useTheme} from "@mui/material"; import Item from "../../components/Item"; import {useTranslation} from "react-i18next"; import {DataGrid, GridColDef, GridRowId, GridRowSelectionModel} from "@mui/x-data-grid"; -import {useState} from "react"; +import {useEffect, useState} from "react"; import {Gauge, gaugeClasses} from "@mui/x-charts"; +import {useAccount} from "../AccountProvider.tsx"; +import {useQuery} from "@tanstack/react-query"; +import {fetchItems} from "../query/Queries.tsx"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; export default function ItemsInfo() { const theme = useTheme(); @@ -56,37 +61,25 @@ export default function ItemsInfo() { console.log("IconEdit", item); } - const _rows: Item[] = [ - { - id: "1", - uuid: "uuid123", - name: "Item", - description: "Super duper geil wichtiger text ", - price100: 1000, - stock: 100, - stockExpected: 1200, - category: "garden", - rating: 7, - discount100: 21, - }, - { - id: "2", - uuid: "uuid12312412", - name: "Schlauch", - description: "Schlauchiger Schlauch für Schlauchige Angelegenheiten", - price100: 10, - stock: 50, - stockExpected: 100, - category: "technicalComponents", - rating: 10, - discount100: 21, - }, - ] - //TODO: get per REST - const [rows, setRows] = useState(_rows); + const [rows, setRows] = useState([]); const [selectedRows, setSelectedRows] = useState>(new Set()); + const {user: loginData} = useAccount(); + + const { data } = useQuery({ + queryKey: ["fetchItems", loginData], + queryFn: () => fetchItems(loginData? loginData : {email: "", password: "", session: "", customerId: -1}), + retry: 3, + retryDelay: 1000, + }); + + useEffect(() => { + if (data) { + setRows(data); + } + }, [data]); + const handleSelectionChange = (newSelection: GridRowSelectionModel) => { setSelectedRows(newSelection.ids); }; @@ -112,7 +105,7 @@ export default function ItemsInfo() { { field: 'name', headerName: t('name'), - width: 150, + width: 200, editable: true, }, { @@ -120,6 +113,7 @@ export default function ItemsInfo() { headerName: t('category'), width: 150, editable: true, + valueFormatter: (val) => t(val), }, { field: 'description', @@ -132,7 +126,8 @@ export default function ItemsInfo() { headerName: t('price100'), width: 100, editable: true, - type: 'number' + type: 'number', + valueFormatter: (val) => (val / 100).toFixed(2), }, { field: 'discount100', diff --git a/01-frontend/src/helper/navbar/NavBar.tsx b/01-frontend/src/helper/navbar/NavBar.tsx index f101cf4..e003dfb 100644 --- a/01-frontend/src/helper/navbar/NavBar.tsx +++ b/01-frontend/src/helper/navbar/NavBar.tsx @@ -1,7 +1,7 @@ import AdbIcon from '@mui/icons-material/Adb'; import MenuIcon from '@mui/icons-material/Menu'; import SearchIcon from '@mui/icons-material/Search'; -import { Autocomplete, TextField } from '@mui/material'; +import { Autocomplete, Badge, TextField } from '@mui/material'; import AppBar from '@mui/material/AppBar'; import Avatar from '@mui/material/Avatar'; import Box from '@mui/material/Box'; @@ -22,15 +22,22 @@ import { useAccount } from '../AccountProvider'; import { fetchItemList } from '../query/Queries'; import LoginDialog from './LoginDialog'; import './NavBar.css'; +import { useBasket } from '../BasketProvider'; export default function NavBar() { const { t } = useTranslation(); const navigate = useNavigate(); const [anchorElNav, setAnchorElNav] = React.useState(null); const [anchorElUser, setAnchorElUser] = React.useState(null); + const [avatarName, setAvatarName] = React.useState(''); // Für Avatar-Tooltip const { user, logout } = useAccount(); + const { basket } = useBasket(); + + const totalQuantity = basket?.reduce((sum, item) => sum + item.quantity, 0) ?? 0; + + const [loginOpen, setLoginOpen] = React.useState(false); const [loginData, setLoginData] = React.useState({ password: '', email: '', customerId: 0 }); @@ -42,6 +49,11 @@ export default function NavBar() { const pages = pageKeys.map(key => ({ key, label: t(key) })); const settings = user ? [ + { + key: 'email', + label: `${t('loggedInAs')}: ${user.email}`, + disabled: true // wir nutzen dieses Flag gleich zur Erkennung + }, { key: 'account', label: t('account') }, { key: 'orders', label: t('orders') }, { key: 'logout', label: t('logout') } @@ -96,6 +108,13 @@ export default function NavBar() { setItemNames(items.map((item) => item.name)); }, [items]); + React.useEffect(() => { + // Setze den Avatar-Namen, wenn der Benutzer angemeldet ist + if (user) { + setAvatarName(user.email.toUpperCase()); + } + }, [user]); + const handleSearch = (_: React.SyntheticEvent, value: string | null) => { if (!value) { @@ -178,15 +197,36 @@ export default function NavBar() { - {pages.map(({ key, label }) => ( - - ))} + {pages.map(({ key, label }) => { + if (key === 'checkout') { + return ( + + ); + } + + return ( + + ); + })} @@ -216,7 +256,7 @@ export default function NavBar() { - + setAnchorElUser(null)} > - {settings.map(({ key, label }) => ( - handleCloseUserMenu(key)}> + {settings.map(({ key, label, disabled }) => ( + { + if (!disabled) handleCloseUserMenu(key); + }} + disabled={disabled} + > {label} ))} diff --git a/01-frontend/src/helper/productpage/ProductInfo.tsx b/01-frontend/src/helper/productpage/ProductInfo.tsx index 01a3325..877e76a 100644 --- a/01-frontend/src/helper/productpage/ProductInfo.tsx +++ b/01-frontend/src/helper/productpage/ProductInfo.tsx @@ -59,11 +59,14 @@ export default function ProductInfo({ item }: { item: Item }) { const fetchImage = async () => { try { const response = await fetch(`http://localhost:8085/image?uuid=${item.uuid}`); - const data = await response.text(); + var data = await response.text(); if(data.length == 0) { console.error("Got emtpy picture for article ", item.uuid); } - setImageUrl("data:image/jpeg;base64," + data); + if(!data.startsWith("data:image/")) { + data = "data:image/jpeg;base64," + data + } + setImageUrl(data); } catch (error) { console.error("Fehler beim Laden des Bildes:", error); } diff --git a/01-frontend/src/helper/query/Queries.tsx b/01-frontend/src/helper/query/Queries.tsx index a3e1242..99b718b 100644 --- a/01-frontend/src/helper/query/Queries.tsx +++ b/01-frontend/src/helper/query/Queries.tsx @@ -157,4 +157,12 @@ export const fetchAccounts = async (loginData: User) => { throw new Error("Login failed"); } return response.json(); +}; + +export const fetchItems = async (loginData: User) => { + const response = await fetch("http://localhost:8085/article/all?email=" + loginData.email + "&session=" + loginData.session); + if (!response.ok) { + throw new Error("Login failed"); + } + return response.json(); }; \ No newline at end of file diff --git a/01-frontend/src/pages/Payment.tsx b/01-frontend/src/pages/Payment.tsx index 63673b4..e67a10c 100644 --- a/01-frontend/src/pages/Payment.tsx +++ b/01-frontend/src/pages/Payment.tsx @@ -15,17 +15,17 @@ import { TextField, Typography } from '@mui/material'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { TFunction } from 'i18next'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from "react-i18next"; import { useNavigate } from 'react-router-dom'; -import Item from '../components/Item'; -import { BasketItem, useBasket } from '../helper/BasketProvider'; -import { OrderStatusEnum, ShippingDetails, SubmitOrder } from '../components/Order'; -import { useAccount } from '../helper/AccountProvider'; -import { useQuery, useMutation } from '@tanstack/react-query'; -import { submitCustomer, submitOrder } from '../helper/query/Queries'; import { CustomerType } from '../components/Account'; +import Item from '../components/Item'; +import { OrderStatusEnum, SubmitOrder } from '../components/Order'; +import { useAccount } from '../helper/AccountProvider'; +import { BasketItem, useBasket } from '../helper/BasketProvider'; +import { fetchCustomer, submitCustomer, submitOrder } from '../helper/query/Queries'; function getDiscountedPrice(item: Item): number { return (item.price100 / 100 * (100-item.discount100)/100); @@ -70,26 +70,17 @@ export default function Payment() { const { basket, clearBasket } = useBasket(); const navigator = useNavigate(); const [activeStep, setActiveStep] = useState(0); - const [shippingDetails, setShippingDetails] = useState({ - firstName: '', - lastName: '', - telefon: '', + const [shippingDetails, setShippingDetails] = useState({ + id: 0, // This will be set by the backend or user data + name: '', + surname: '', address: '', - postalCode: '', - city: '', + zip: '', country: 'Deutschland', }); const [orderNumber, setOrderNumber] = useState(null); const steps = [t('reviewCart'), t('shippingDetails'), t('payment'), t('orderSummary')]; const { user } = useAccount(); - const customerData: CustomerType = { - id: 0, - name: shippingDetails.firstName, - surname: shippingDetails.lastName, - address: shippingDetails.address, - country: shippingDetails.country, - zip: shippingDetails.postalCode, - }; const submitOrderData: SubmitOrder = { id: 0, // This will be set by the backend @@ -104,8 +95,8 @@ export default function Payment() { }; const { refetch: refetchCustomer } = useQuery({ - queryKey: ["submitCustomer", customerData], - queryFn: () => submitCustomer(customerData), + queryKey: ["submitCustomer", shippingDetails], + queryFn: () => submitCustomer(shippingDetails), retry: 0, retryDelay: 1000, enabled: false, @@ -117,6 +108,28 @@ export default function Payment() { }; + const { refetch: customerData } = useQuery({ + queryKey: ['fetchCustomer', user?.customerId], + queryFn: () => fetchCustomer(user?.customerId || 0), // Funktion zum Abrufen der Kundendaten + enabled: false + }); + + useEffect(() => { + const fetchShippingDetails = async () => { + if (user) { + try { + const userShippingDetails = (await customerData()).data; + setShippingDetails(userShippingDetails || shippingDetails); + } catch (error) { + console.error("Fehler beim Laden der Kundendaten:", error); + } + } + }; + + fetchShippingDetails(); + }, [user, customerData, shippingDetails]); + + // Verwende useMutation statt useQuery für submitOrder const { mutateAsync: submitOrderMutation } = useMutation({ mutationFn: (orderData: SubmitOrder) => submitOrder(orderData), @@ -174,11 +187,10 @@ export default function Payment() { // Hilfsfunktion prüfen, ob alle Pflichtfelder ausgefüllt sind const isShippingDetailsValid = () => { return ( - shippingDetails.firstName.trim() !== '' && - shippingDetails.lastName.trim() !== '' && + shippingDetails.name.trim() !== '' && + shippingDetails.surname.trim() !== '' && shippingDetails.address.trim() !== '' && - shippingDetails.postalCode.trim() !== '' && - shippingDetails.city.trim() !== '' && + shippingDetails.zip.trim() !== '' && shippingDetails.country.trim() !== '' ); }; @@ -210,8 +222,8 @@ export default function Payment() { @@ -219,21 +231,12 @@ export default function Payment() { - - - - @@ -299,10 +292,9 @@ export default function Payment() { {t('shippingDetails')}: - {shippingDetails.firstName} {shippingDetails.lastName}
+ {shippingDetails.name} {shippingDetails.surname}
{shippingDetails.address}
- {shippingDetails.postalCode} {shippingDetails.city}
- {shippingDetails.country} + {shippingDetails.zip} {shippingDetails.country}
{t('orderedItems')}: diff --git a/README.md b/README.md index 6a3b1d5..6381b7d 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ Webshop-Projekt für htw saar Digitale Produktionssysteme ### 🤮 Windows -- TODO: make script +- Check if in parent folder, because after exit script is in frontend folder. So retry after cd .. only possible ```shell - + ./start.ps1 ``` # Contributors diff --git a/start.ps1 b/start.ps1 new file mode 100644 index 0000000..0a3311f --- /dev/null +++ b/start.ps1 @@ -0,0 +1,60 @@ +# Funktion zur Bereinigung beim Beenden +function OnExit { + Write-Host "`n[DPS] Cleaning up..." + if ($backendProcess -and !$backendProcess.HasExited) { + $backendProcess.Kill() + } + if ($frontendProcess -and !$frontendProcess.HasExited) { + $frontendProcess.Kill() + } + Write-Host "[DPS] Cleaned up..." + + # Einen Ordner höher gehen + Set-Location (Resolve-Path "..").Path + Write-Host "[DPS] Returned to parent directory: $(Get-Location)" + + exit +} + +Write-Host "[DPS] trapped" + +# Backend starten +Set-Location ./00-backend +$backendOutLog = "../backend_latest.out.log" +$backendErrLog = "../backend_latest.err.log" +$backendProcess = Start-Process "java" -ArgumentList "-jar", "target/webshop-0.0.1-SNAPSHOT.jar" ` + -RedirectStandardOutput $backendOutLog ` + -RedirectStandardError $backendErrLog ` + -NoNewWindow -PassThru +Write-Host "[DPS] Backend started with PID $($backendProcess.Id)" + +# Frontend starten (ohne cmd.exe) +Set-Location ../01-frontend +$frontendOutLog = "../frontend_latest.out.log" +$frontendErrLog = "../frontend_latest.err.log" + +$frontendProcess = Start-Process "node" ` + -ArgumentList "node_modules/.bin/react-scripts", "start" ` + -RedirectStandardOutput $frontendOutLog ` + -RedirectStandardError $frontendErrLog ` + -NoNewWindow -PassThru + +Write-Host "[DPS] Frontend started with PID $($frontendProcess.Id)" + +Write-Host "[DPS] Ctrl+C to stop" + +Register-EngineEvent PowerShell.Exiting -Action { OnExit } -SupportEvent + +try { + while ($true) { + Start-Sleep -Seconds 1 + if ($backendProcess.HasExited -and $frontendProcess.HasExited) { + break + } +} +} +catch { + if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { + # This block will not execute on CTRL-C + } +}