diff --git a/package.json b/package.json index 4681ee6..f776279 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dayjs": "^1.11.6", "final-form": "^4.20.7", "html-webpack-plugin": "^5.5.0", + "ignore-loader": "^0.1.2", "markdown-to-jsx": "^7.1.7", "mini-css-extract-plugin": "^2.6.1", "react": "^18.2.0", diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index 6cf6478..d279021 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -1,6 +1,6 @@ import axios from "axios"; -export default axios.create({ +export const axiosInstance = axios.create({ baseURL: "http://localhost:5276", timeout: 1000, params : { diff --git a/src/api/declarations.d.ts b/src/api/declarations.d.ts deleted file mode 100644 index 7eb0fc6..0000000 --- a/src/api/declarations.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare type ApiToken = string; // QUESTION: does this make sense, to have this declaration here since we use is in multiple files? \ No newline at end of file diff --git a/src/api/getCustomerInfo.ts b/src/api/getCustomerInfo.ts new file mode 100644 index 0000000..4d0a02b --- /dev/null +++ b/src/api/getCustomerInfo.ts @@ -0,0 +1,32 @@ +import { axiosInstance } from "./axiosInstance"; +import { ApiToken, CustomerInfo } from "./types"; + +const API_TOKEN_NOT_FOUND_ERROR_CODE = 16; + +/** + * Get the data of customer + * + * @param token The token, given from WOnD, of the customer + * @returns the data of the customer + */ +export async function getCustomerInfo(token: ApiToken, id: number): Promise { + + const res = await axiosInstance.get("/getCustomer", { + params: { token, id } + }) + + if (res.data.getCustomer.result == "ERROR"){ + const code: number = res.data.getCustomer.errorCode; + // if (code == API_TOKEN_NOT_FOUND_ERROR_CODE) + // navigate('/') + const msg: string = res.data.getCustomer.message; + throw new Error(`Couldn't get user info\nError code: ${code}\nError message: ${msg}`); + } + + const customerInfo: CustomerInfo = { + name: res.data.getCustomer.customer.description, // yes i know... + balance: res.data.getCustomer.customer.prepayBalanceCash + } + + return customerInfo; +} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000..be6db69 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,3 @@ +export * from "./isTokenValid"; +export * from "./login"; +export * from "./getCustomerInfo"; \ No newline at end of file diff --git a/src/api/isTokenValid.ts b/src/api/isTokenValid.ts index 2ffaac1..ba4209a 100644 --- a/src/api/isTokenValid.ts +++ b/src/api/isTokenValid.ts @@ -1,4 +1,5 @@ -import axiosInstance from "./axiosInstance"; +import { axiosInstance } from "./axiosInstance"; +import { ApiToken } from "./types"; const API_INVALID_SHOP_ID_CODE = 608; @@ -9,7 +10,7 @@ const API_INVALID_SHOP_ID_CODE = 608; * @param token the token of the logged in customer * @returns whether the user's token is still valid for the backend (aka if he is still logged in) */ -export default async function isTokenValid(token: ApiToken): Promise { +export async function isTokenValid(token: ApiToken): Promise{ // There is no official way to check if the token is still valid or not, // so after asking Daniele Comes, he said to make a call to retrieveOrders // with shopId of -1 so that the DB wouldn't be queried. @@ -19,12 +20,10 @@ export default async function isTokenValid(token: ApiToken): Promise { // Daniele Comes said he asked the WOnD team to make an endpoint to ask if the token is valid. - let res = await axiosInstance.get("/retrieveOrders", { - params: { - token, - shopId: -1 - } - }) + let res = await axiosInstance.get("/retrieveOrders", { params: { + token, + shopId: -1 + }}) if (res.data.retrieveOrders.result != "ERROR") throw new Error("The API somehow didn't answer ERROR when asking to retrieve orders from shop with id=-1... This is not normal!"); diff --git a/src/api/login.ts b/src/api/login.ts index 590b9ba..e75e5db 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -1,20 +1,5 @@ -import axiosInstance from "./axiosInstance"; - -interface LoginResponse { - isLogged: boolean; - nextAction(): void; -} - -class FailedLoginError extends Error{ - code: number; - constructor (msg: string, code: number){ - super(msg); - this.code = code; - Object.setPrototypeOf(this, FailedLoginError.prototype) - } -} - -// type ApiToken = string; +import { axiosInstance } from "./axiosInstance"; +import { ApiToken, LoginResponse, FailedLoginError } from "./types"; /** * @@ -22,15 +7,16 @@ class FailedLoginError extends Error{ * @param password the password of the user * @returns */ -export default async function login(user: string, password: string): Promise{ - // const user = useContext(userContext); +export async function login(user: string, password: string): Promise{ const res = await axiosInstance.get("/login", { params: { user, password } }); if (res.data.login.result == "ERROR") { const errCode: number = res.data.login.errorCode; const errMsg: string = res.data.login.message; throw new FailedLoginError(errMsg, errCode); } - return res.data.login.customerProperties.token; + const token: ApiToken = res.data.login.customerProperties.token; + const id: number = res.data.login.customerProperties.id; + return { token, id }; } export { FailedLoginError }; \ No newline at end of file diff --git a/src/api/types/ApiToken.d.ts b/src/api/types/ApiToken.d.ts new file mode 100644 index 0000000..9864fbc --- /dev/null +++ b/src/api/types/ApiToken.d.ts @@ -0,0 +1 @@ +export type ApiToken = string; \ No newline at end of file diff --git a/src/api/types/CustomerInfo.d.ts b/src/api/types/CustomerInfo.d.ts new file mode 100644 index 0000000..66bd2ae --- /dev/null +++ b/src/api/types/CustomerInfo.d.ts @@ -0,0 +1,4 @@ +export type CustomerInfo = { + name: string; + balance: number; +}; \ No newline at end of file diff --git a/src/api/types/FailedLoginError.ts b/src/api/types/FailedLoginError.ts new file mode 100644 index 0000000..dc6dbf6 --- /dev/null +++ b/src/api/types/FailedLoginError.ts @@ -0,0 +1,8 @@ +export class FailedLoginError extends Error { + code: number; + constructor(msg: string, code: number) { + super(msg); + this.code = code; + Object.setPrototypeOf(this, FailedLoginError.prototype); + } +} \ No newline at end of file diff --git a/src/api/types/LoginResponse.d.ts b/src/api/types/LoginResponse.d.ts new file mode 100644 index 0000000..5bbba0f --- /dev/null +++ b/src/api/types/LoginResponse.d.ts @@ -0,0 +1,6 @@ +import { ApiToken } from "./ApiToken"; + +export type LoginResponse = { + token: ApiToken; + id: number; +}; \ No newline at end of file diff --git a/src/api/types/index.ts b/src/api/types/index.ts new file mode 100644 index 0000000..bac4b31 --- /dev/null +++ b/src/api/types/index.ts @@ -0,0 +1,4 @@ +export * from "./ApiToken.d" +export * from "./CustomerInfo.d" +export * from "./LoginResponse.d" +export * from "./FailedLoginError" \ No newline at end of file diff --git a/src/components/ErrorPage.tsx b/src/components/ErrorPage.tsx index e1796ed..d592440 100644 --- a/src/components/ErrorPage.tsx +++ b/src/components/ErrorPage.tsx @@ -6,7 +6,7 @@ type ErrorProps = { err?: Error } -export default function ErrorPage({err} : ErrorProps) { +export function ErrorPage({err} : ErrorProps) { const error: any = err || useRouteError(); console.error(error); diff --git a/src/components/ForgotPassword.tsx b/src/components/ForgotPassword.tsx index 868937b..ec20872 100644 --- a/src/components/ForgotPassword.tsx +++ b/src/components/ForgotPassword.tsx @@ -22,7 +22,7 @@ const schema = yup }) .required(); -export default function ForgotPassword() { +export function ForgotPassword() { const navigate = useNavigate(); const { register, diff --git a/src/components/Login.tsx b/src/components/Login.tsx index fc582ed..b810621 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useContext, useEffect } from "react"; import { Card, Button, TextField } from "@mui/material"; import "@scss/login.scss"; import { Link, useNavigate } from "react-router-dom"; @@ -7,7 +7,9 @@ import * as yup from "yup"; import { useForm } from "react-hook-form"; import userContext from "@ts/userContext" import Typography from "@theme/modules/components/Typography"; -import login , { FailedLoginError } from "@api/login"; +import { FailedLoginError } from "@api/types/FailedLoginError"; +import { LoginResponse } from "@api/types"; +import { login } from "@api"; type Inputs = { email: string; @@ -25,7 +27,7 @@ const schema = yup }) .required(); -export default function Login() { +export function Login() { const user = useContext(userContext); const navigate = useNavigate(); const { @@ -58,6 +60,7 @@ export default function Login() { name: "password", id: "password", label: "Password", + type: "password", error: !!errors.password, helperText: errors?.password?.message, }; @@ -66,18 +69,27 @@ export default function Login() { console.warn("There will be an API call now with the following data"); console.table(data); try { - const token: ApiToken = await login(data.email, data.password); - user.setToken(token); - window.localStorage.setItem("token", token); // this could be done with a hook (right?) - navigate("/"); + const res: LoginResponse = await login(data.email, data.password); + console.log("Response of login:", res); + user.setToken(res.token); + user.setId(res.id); + console.log("User context after login:", user) + window.localStorage.setItem("token", res.token); // this could be done with a hook (right?) + window.localStorage.setItem("customerId", res.id.toString()); // this could be done with a hook (right?) }catch(err){ + if (err instanceof FailedLoginError) - alert(`Failed to login user.\nError code: '${err.code}', error message: '${err.message}'`) + alert(`Failed to login user.\nError code: '${err.code}', error message: '${err.message}'\n\nYes, I know I should handle it with something else than an alert, but for now I'm making the thing work When it fully works, I'll make it prettier :)`) else - alert('Failed to make api call'); + alert(`Failed to make api call\nError message: ${err.message}`); } } + useEffect(() => { + if (user.token) + navigate("/"); + }, [user.token]) + return (
diff --git a/src/components/Reset.tsx b/src/components/Reset.tsx index 1431752..91180f6 100644 --- a/src/components/Reset.tsx +++ b/src/components/Reset.tsx @@ -22,7 +22,7 @@ const schema = yup }) .required(); -export default function Reset() { +export function Reset() { const navigate = useNavigate(); const { diff --git a/src/components/Transactions.tsx b/src/components/Transactions.tsx index 33b5dee..8e90187 100644 --- a/src/components/Transactions.tsx +++ b/src/components/Transactions.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useState } from "react"; import "@scss/transactions.scss"; import Table from "@components/Transactions/Table" import userContext from "@ts/userContext" @@ -15,6 +15,8 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider/L import dayjs, { Dayjs } from 'dayjs'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { getValue } from "@mui/system"; +import { CustomerInfo } from "@api/types"; +import { getCustomerInfo } from "@api"; @@ -30,7 +32,7 @@ type Inputs = { const schema = yup .object() .shape({ - startDate: yup.date().nullable().default(() => {return null}).typeError("Invalid date"), + startDate: yup.date().nullable().typeError("Invalid date"), endDate: yup.date().nullable().typeError("Invalid date"), minAmount: yup.number().positive(), maxAmount: yup.number().positive(), @@ -39,8 +41,20 @@ const schema = yup .required(); -export default function Transactions() { - const user = useContext(userContext) +export function Transactions() { + const user = useContext(userContext); + const [customerInfo, setCustomerInfo] = useState({name: "John Smith", balance: -1}); + + async function fetchData() { + if (!user.token) + return; + const info = await getCustomerInfo(user.token, user.id); + setCustomerInfo(info); + }; + + useEffect(() => { + fetchData(); + }, [user.token]) const { register, @@ -53,7 +67,7 @@ export default function Transactions() { const { ref: startDateRef, ...startDateRegisterProps } = register("startDate"); - console.log(startDateRegisterProps) + // console.log(startDateRegisterProps) const startDatePickerProps = { ...startDateRegisterProps, @@ -116,7 +130,7 @@ export default function Transactions() { - John Smith + {customerInfo.name} (CHF {customerInfo.balance}) diff --git a/src/components/index.tsx b/src/components/index.tsx index ac0b0f8..a21b603 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -1,9 +1,7 @@ -import Login from "@components/Login"; -import ForgotPassword from "@components/ForgotPassword"; -import Reset from "@components/Reset"; -import ErrorPage from "@components/ErrorPage"; -import Transactions from "@components/Transactions"; -import MetadataSetter from "@components/lib/MetadataSetter"; -import AuthComponent from "@components/lib/AuthComponent"; - -export { Login, ForgotPassword, Reset, ErrorPage, Transactions, MetadataSetter, AuthComponent }; \ No newline at end of file +export * from "@components/Login"; +export * from "@components/ForgotPassword"; +export * from "@components/Reset"; +export * from "@components/ErrorPage"; +export * from "@components/Transactions"; +export * from "@components/lib/MetadataSetter"; +export * from "@components/lib/AuthComponent"; \ No newline at end of file diff --git a/src/components/lib/AuthComponent.tsx b/src/components/lib/AuthComponent.tsx index 27d46dc..11a2389 100644 --- a/src/components/lib/AuthComponent.tsx +++ b/src/components/lib/AuthComponent.tsx @@ -1,23 +1,28 @@ -import isTokenValid from "@api/isTokenValid"; -import ErrorPage from "@components/ErrorPage"; -import React, { useEffect } from "react"; +import { isTokenValid } from "@api"; +import { ApiToken } from "@api/types"; +import userContext from "@ts/userContext"; +import React, { useContext, useEffect } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; type AuthProps = { children: React.ReactNode, } -export default function AuthComponent({ children }: AuthProps) { +export function AuthComponent({ children }: AuthProps) { const navigate = useNavigate(); + const user = useContext(userContext); + + async function checkIfCustomerLogged(token: ApiToken){ + const isLogged: boolean = token != null && await isTokenValid(user.token); + + if (!isLogged){ + navigate("/login", { replace: true }); + } + } useEffect(() => { // navigate needs to be wrapped in a useEffect so that it gets executed after the component is mounted. Otherwise it doesn't redirect - (async () => { - const token: ApiToken = window.localStorage.getItem("token"); - const isLogged: boolean = token != null && await isTokenValid(token); - if (!isLogged) - navigate("/login", { replace: true }); - })() - }, []) + checkIfCustomerLogged(user.token); + }, [user.token]) // by default return the children, the effect will check if the user needs to be redirected return <>{children}; diff --git a/src/components/lib/MetadataSetter.tsx b/src/components/lib/MetadataSetter.tsx index 174ae61..aa8ee18 100644 --- a/src/components/lib/MetadataSetter.tsx +++ b/src/components/lib/MetadataSetter.tsx @@ -1,16 +1,17 @@ import React, { useEffect } from "react"; -import {AuthComponent} from "@components"; +import { AuthComponent } from "@components"; type MetadataProps = { children: JSX.Element | JSX.Element[], title: string, bodyClass: string, - needsAuth? : boolean + needsAuth?: boolean } -export default function MetadataSetter({children, title, bodyClass, needsAuth = false}: MetadataProps){ +export function MetadataSetter({ children, title, bodyClass, needsAuth = false }: MetadataProps) { + useEffect(() => { - document.title= title; + document.title = title; document.body.classList.add(bodyClass); }, [title]) diff --git a/src/index.tsx b/src/index.tsx index 9922ab5..da180f0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { MetadataSetter, ErrorPage, Login, ForgotPassword, Reset, Transactions } from "@components"; import UserContext from '@ts/userContext' +import { ApiToken } from "@api/types"; const router = createBrowserRouter([ { @@ -41,8 +42,11 @@ const router = createBrowserRouter([ const App = function () { - const [token, setToken] = useState() - const userContextValue = { token, setToken } + + const [token, setToken] = useState(window.localStorage.getItem("token")); + const [id, setId] = useState(Number.parseInt(window.localStorage.getItem("customerId"))); + const userContextValue = { token, setToken, id, setId } + return ( diff --git a/src/ts/userContext.ts b/src/ts/userContext.ts index 8d3bb21..53c98a3 100644 --- a/src/ts/userContext.ts +++ b/src/ts/userContext.ts @@ -2,5 +2,7 @@ import { createContext } from 'react' export default createContext({ token : "Unknown", - setToken: (newToken: string) => {} + setToken: (newToken: string) => {}, + id : -1, + setId: (newId: number) => {}, }) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 09907cf..de1de1d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "@components/*": ["src/components/*"], "@scss/*": ["src/scss/*"], "@ts/*": ["src/ts/*"], + "@api": ["src/api"], "@api/*": ["src/api/*"], "@theme/*": ["themes/onepirate/*"], } diff --git a/webpack.config.js b/webpack.config.js index a93cf91..5ae3743 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,9 +25,13 @@ module.exports = (env, argv) => { module: { rules: [ { - test: /\.tsx?/, - use: 'ts-loader', - exclude: /node_modules/ + test: /\.tsx?$/, + loader: 'ts-loader', + exclude: /node_modules|\.d\.ts$/ + }, + { + test: /\.d\.ts$/, + loader: 'ignore-loader' }, { test: /\.jsx?$/,