diff --git a/inventory-manager/src/App.jsx b/inventory-manager/src/App.jsx index b8e3c0846b6c4ccfb20a498f3118af4789561ffc..481aaeeb1bf8b67f320cf92ce4c55befadf26082 100644 --- a/inventory-manager/src/App.jsx +++ b/inventory-manager/src/App.jsx @@ -8,17 +8,32 @@ import { Register, UpdatePassword, } from "./components/user/index"; +import { useState } from "react"; +import useToken from "./components/useToken"; +import AccountInformation from "./components/user/AccountInformation"; +import ProtectedRoute from "./routes/ProtectedRoute"; + function App() { + // const [token, setToken] = useState(); + const { token, setToken } = useToken(); + // const token = getToken(); + // if (!token) + // { + // return <Login setToken={setToken}/> + // } return ( <div className="App"> <Navbar /> <Routes> <Route path="/" element={<Home />} /> + <Route path="/login" element={<Login setToken={setToken}/>} /> <Route path="/register" element={<Register />} /> - <Route path="/login" element={<Login />} /> - <Route path="/deleteUser" element={<DeleteUser />} /> + <Route path='/accountinfo' element={<ProtectedRoute token={token}> <AccountInformation token={token}/> </ProtectedRoute>}> + </Route> + {/* <Route path="/deleteUser" element={<DeleteUser token={token}/>} /> <Route path="/updatepassword" element={<UpdatePassword />} /> + <Route path="/accountinfo" element={<AccountInformation token={token}/> } /> */} </Routes> </div> ); diff --git a/inventory-manager/src/components/Home.css b/inventory-manager/src/components/Home.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2cd23b24eab89b43a26c3cb354669a5112541304 100644 --- a/inventory-manager/src/components/Home.css +++ b/inventory-manager/src/components/Home.css @@ -0,0 +1,24 @@ +/* Add some basic styling for better presentation */ +.home-container { + max-width: 90%; + margin: 0 auto; + padding: 20px; + text-align: center; +} + +h1 { + color: #007bff; +} + +h2 { + color: #333; +} + +ul { + list-style-type: none; + padding: 0; +} + +li { + margin-bottom: 8px; +} diff --git a/inventory-manager/src/components/Home.jsx b/inventory-manager/src/components/Home.jsx index e3b897e12dda64d4fcb1389de3e18fa9db8a6a70..cf97695904d806905c4d0bb5fd146567cb5d3a53 100644 --- a/inventory-manager/src/components/Home.jsx +++ b/inventory-manager/src/components/Home.jsx @@ -1,6 +1,24 @@ -import "./Home.css"; import React from "react"; +import "./Home.css"; export const Home = () => { - return <div>Home</div>; + return ( + <div className="home-container"> + <h1>Welcome to inVT</h1> + <p> + inVT is the Virginia Tech Student Organization Inventory Management System. + Manage your organization's inventory efficiently with our user-friendly platform. + </p> + <h2>Key Features:</h2> + <ul> + <li>Create Organizations</li> + <li>Manage Inventory</li> + <li>Buy and Sell Items with Other Clubs/People</li> + </ul> + <p> + Streamline your organization's operations and transactions seamlessly. + Join inVT today for a better inventory management experience! + </p> + </div> + ); }; diff --git a/inventory-manager/src/components/Navbar.jsx b/inventory-manager/src/components/Navbar.jsx index a80a03812e335c3d8da08a6d9773b87857e5b806..201968a58fe2d32a8dea784b3ea5f1155bb5b811 100644 --- a/inventory-manager/src/components/Navbar.jsx +++ b/inventory-manager/src/components/Navbar.jsx @@ -31,6 +31,9 @@ export const Navbar = () => { <li> <NavLink to="/updatepassword">Update Password</NavLink> </li> + <li> + <NavLink to="/accountinfo">Account Information</NavLink> + </li> </ul> </nav> ); diff --git a/inventory-manager/src/components/useToken.jsx b/inventory-manager/src/components/useToken.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5c0106a4ecedbdd8cdd2db05179d962f20302358 --- /dev/null +++ b/inventory-manager/src/components/useToken.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import Axios from "axios"; + +export default function useToken() { + const getToken = () => { + const tokenString = sessionStorage.getItem('token'); + if (!tokenString) + return null; + console.log(tokenString); + const userToken = JSON.parse(tokenString); + console.log(userToken); + if (userToken.jwt != null) + { + Axios.post("http://localhost:8080/auth/verify", { + jwt: userToken.jwt + }).then((response) => { + console.log(response); + if (response.user) + return response; + }); + } + return null; + }; + + const [token, setToken] = useState(getToken()); + + const saveToken = userToken => { + sessionStorage.setItem('token', JSON.stringify(userToken.data)); + setToken(userToken.data); + }; + + return { + setToken: saveToken, + token + } +} \ No newline at end of file diff --git a/inventory-manager/src/components/user/AccountInformation.css b/inventory-manager/src/components/user/AccountInformation.css new file mode 100644 index 0000000000000000000000000000000000000000..b44c18c9862137588e0ea6f70e6f0a13f2319804 --- /dev/null +++ b/inventory-manager/src/components/user/AccountInformation.css @@ -0,0 +1,43 @@ +/* AccountInformation.css */ + +.account-info-container { + max-width: 400px; + margin: 0 auto; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; +} + +.info-form { + display: flex; + flex-direction: column; +} + +label { + margin-bottom: 8px; +} + +input { + padding: 8px; + margin-bottom: 16px; + border: 1px solid #ccc; + border-radius: 4px; +} + +button { + padding: 10px; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} + +.success-message { + color: green; + margin-top: 10px; +} \ No newline at end of file diff --git a/inventory-manager/src/components/user/AccountInformation.jsx b/inventory-manager/src/components/user/AccountInformation.jsx new file mode 100644 index 0000000000000000000000000000000000000000..61e8007769a936e361a8c56925789670c3ec6c3e --- /dev/null +++ b/inventory-manager/src/components/user/AccountInformation.jsx @@ -0,0 +1,127 @@ +import React, { useState, useEffect } from "react"; +import Axios from "axios"; +import PropTypes from "prop-types"; +import './AccountInformation.css'; // Import your external CSS file + +const AccountInformation = ({ token }) => { + const [userInfo, setUserInfo] = useState({ + fname: "", + lname: "", + password: "", + phoneNumber: "", + email: "", + }); + + const [updateSuccess, setUpdateSuccess] = useState(false); + + useEffect(() => { + // Fetch user information when the component mounts + getUserInfo(); + }, []); // Empty dependency array ensures the effect runs once + + const getUserInfo = async () => { + try { + const response = await Axios.post( + "http://localhost:8080/user/user", + { jwt: token.jwt } + ); + setUserInfo(response.data); + } catch (error) { + console.error("Error fetching user information:", error); + } + }; + + const handleUpdate = async () => { + try { + const response = await Axios.put( + "http://localhost:8080/user/update", + { + fname: userInfo.fname, + lname: userInfo.lname, + password: userInfo.password, + phoneNumber: userInfo.phoneNumber, + email: userInfo.email, + jwt: token.jwt, + } + ); + setUserInfo(response.data); + setUpdateSuccess(true); + console.log("User information updated successfully"); + } catch (error) { + console.error("Error updating user information:", error); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setUserInfo((prevUserInfo) => ({ + ...prevUserInfo, + [name]: value, + })); + }; + + return ( + <div className="account-info-container"> + <h2>Account Information</h2> + {updateSuccess && <p className="success-message">Information updated successfully!</p>} + <div className="info-form"> + <label htmlFor="fname">First Name</label> + <input + type="text" + id="fname" + name="fname" + value={userInfo.fname} + onChange={handleChange} + /> + + <label htmlFor="lname">Last Name</label> + <input + type="text" + id="lname" + name="lname" + value={userInfo.lname} + onChange={handleChange} + /> + + <label htmlFor="password">Password</label> + <input + type="password" + id="password" + name="password" + value={userInfo.password} + onChange={handleChange} + /> + + <label htmlFor="phoneNumber">Phone Number</label> + <input + type="text" + id="phoneNumber" + name="phoneNumber" + value={userInfo.phoneNumber || ""} + onChange={handleChange} + /> + + <label htmlFor="email">Email</label> + <input + type="email" + id="email" + name="email" + value={userInfo.email} + onChange={handleChange} + /> + + <button type="button" onClick={handleUpdate}> + Update Information + </button> + </div> + </div> + ); +}; + +AccountInformation.propTypes = { + token: PropTypes.shape({ + jwt: PropTypes.string.isRequired, + }).isRequired, +}; + +export default AccountInformation; diff --git a/inventory-manager/src/components/user/DeleteUser.jsx b/inventory-manager/src/components/user/DeleteUser.jsx index ac4b9cdc641af8153bb2e6dc4b7b69128818c930..3121abbe7a9e89d38394ebcc0f610bf71832fbe8 100644 --- a/inventory-manager/src/components/user/DeleteUser.jsx +++ b/inventory-manager/src/components/user/DeleteUser.jsx @@ -1,28 +1,22 @@ import React, { useState } from "react"; import Axios from "axios"; +import useToken from "../useToken"; -export const DeleteUser = () => { +export const DeleteUser = (token) => { const [email, setEmail] = useState(""); - + // console.log(JSON.parse(token.token).user); + // console.log(token.token.user); + console.log(token); const handleSubmit = (e) => { e.preventDefault(); - - Axios.delete("http://localhost:8080/user/delete", { - headers: {}, - data: { - email: email - } + Axios.post("http://localhost:8080/user/delete", { + jwt: token.token.jwt }).then((response) => { - console.log(response); + // console.log(response); + // if (response.data.email == "") }); }; - // Axios.delete("http://localhost:8080/user/delete", { - // email: email, - // }).then((response) => { - // console.log(response); - // }); - // }; return ( <div className="auth-form-container"> diff --git a/inventory-manager/src/components/user/Login.css b/inventory-manager/src/components/user/Login.css new file mode 100644 index 0000000000000000000000000000000000000000..330234b252f41632a6ac8fbefd2b7e5e854f40ee --- /dev/null +++ b/inventory-manager/src/components/user/Login.css @@ -0,0 +1,44 @@ +/* Login.css */ + +.auth-form-container { + max-width: 400px; + margin: 0 auto; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; +} + +.login-form { + display: flex; + flex-direction: column; +} + +label { + margin-bottom: 8px; +} + +input { + padding: 8px; + margin-bottom: 16px; + border: 1px solid #ccc; + border-radius: 4px; +} + +button { + padding: 10px; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} + +/* Add this style for error messages */ +.error-message { + color: red; + margin-top: 10px; +} \ No newline at end of file diff --git a/inventory-manager/src/components/user/Login.jsx b/inventory-manager/src/components/user/Login.jsx index 694d9a729e8b9aeaee5ab98937e05cdb1815608c..df508c1315f8d5e79ade9b4e078a2658ef909fdd 100644 --- a/inventory-manager/src/components/user/Login.jsx +++ b/inventory-manager/src/components/user/Login.jsx @@ -1,15 +1,37 @@ import React, { useState } from "react"; import Axios from "axios"; +import PropTypes from "prop-types"; +import { Link } from "react-router-dom"; // Import Link from React Router +import './Login.css'; // Import your external CSS file -export const Login = (props) => { - const [email, setEmail] = useState(""); - const [pass, setPass] = useState(""); +export const Login = ({ setToken }) => { + const [email, setEmail] = useState(); + const [pass, setPass] = useState(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); const handleSubmit = (e) => { e.preventDefault(); - console.log(email); - console.log(pass); - + setLoading(true); + setError(""); + Axios.post("http://localhost:8080/auth/login", { + email: email, + password: pass, + }).then((response) => { + console.log(response); + // console.log(response.data.login); + if (response.data.result === "success") { + setToken(response); + } else { + setError("Invalid email or password. Please try again."); + } + }).catch((error) => { + setError("An error occurred. Please try again later."); + console.error("Login error:", error); + }) + .finally(() => { + setLoading(false); + }); }; return ( @@ -34,10 +56,21 @@ export const Login = (props) => { id="password" name="password" /> - <button type="submit">Log In</button> + {loading ? ( + <p>Loading...</p> + ) : ( + <button type="submit">Log In</button> + )} + {error && <p className="error-message">{error}</p>} + {/* Add a Link to the Register page */} + <p>Don't have an account? <Link to="/register">Register here</Link></p> </form> </div> ); +}; + +Login.propTypes = { + setToken: PropTypes.func.isRequired }; export default Login; diff --git a/inventory-manager/src/routes/ProtectedRoute.jsx b/inventory-manager/src/routes/ProtectedRoute.jsx new file mode 100644 index 0000000000000000000000000000000000000000..01d582e8777338ce45991e50d2681c15238104f0 --- /dev/null +++ b/inventory-manager/src/routes/ProtectedRoute.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Route, Navigate } from 'react-router-dom'; + +const ProtectedRoute = ({ element, token, ...props }) => { + return token ? ( + <Route {...props} element={element} /> + ) : ( + <Navigate to="/login" /> + ); +}; + +export default ProtectedRoute; diff --git a/pom.xml b/pom.xml index 5a5b5d9c8adbf40ac59868d6f0b197bce0fe6f6f..a030a6cbdb35c71fd8d3c832c9f3291f570d2011 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,33 @@ <artifactId>jackson-databind</artifactId> <version>2.14.2</version> </dependency> + <!-- JAX-RS Implementation (for example, Jersey) --> + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>3.0.3</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-devtools</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt</artifactId> + <version>0.9.1</version> + </dependency> + <!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api --> + <dependency> + <groupId>javax.xml.bind</groupId> + <artifactId>jaxb-api</artifactId> + <version>2.3.1</version> + </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-configuration-processor</artifactId> + <optional>true</optional> + </dependency> </dependencies> <build> diff --git a/src/main/java/com/example/accessingdatamysql/MainController.java b/src/main/java/com/example/accessingdatamysql/MainController.java index 9e226655bdef9125834bf786aba937a376fb7939..0fb782e6733d1e312def9a08cfe92aa7684cd9f4 100644 --- a/src/main/java/com/example/accessingdatamysql/MainController.java +++ b/src/main/java/com/example/accessingdatamysql/MainController.java @@ -7,11 +7,14 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.Map; +import java.util.HashMap; +import io.jsonwebtoken.Claims; +import com.example.accessingdatamysql.auth.AuthController; import java.util.Optional; @CrossOrigin @RestController // This means that this class is a Controller -@RequestMapping(path="/user") // This means URL's start with /demo (after Application path) +@RequestMapping(path="/user") // This means URL's start with /user (after Application path) public class MainController { @Autowired // This means to get the bean called userRepository // Which is auto-generated by Spring, we will use it to handle the data @@ -60,28 +63,48 @@ public class MainController { return userRepository.findAll(); } - @GetMapping(path = "/user") - public @ResponseBody Optional<User> getUser(@RequestBody Map<String, String> json) + @PostMapping(path = "/user") + public @ResponseBody User getUser(@RequestBody Map<String, String> json) { - String email = json.get("email"); - return userRepository.findById(email); + User found = new User(); + AuthController au = new AuthController(); + Map<String, String> res = au.verify(json); // if the jwt token could not be verified + if (res.containsKey("login") && res.get("login").equals("failed")) + { + found.setEmail("failed"); + return found; + } + Optional<User> usr = userRepository.findById(res.get("user")); + if (!usr.isPresent()) + { + found.setEmail("not found"); + return found; + } + return usr.get(); } - @DeleteMapping(path = "/delete") - public @ResponseBody User deleteUser(@RequestBody Map<String, String> json) + @PostMapping(path = "/delete") + @ResponseBody + public User deleteUser(@RequestBody Map<String, String> json) { - User found = null; - String email = json.get("email"); + User found = new User(); + AuthController au = new AuthController(); + Map<String, String> res = au.verify(json); // if the jwt token could not be verified + if (res.containsKey("login") && res.get("login").equals("failed")) + { + found.setEmail("failed"); + return found; + } + String email = res.get("user"); Optional<User> optionalUser = userRepository.findById(email); - if (optionalUser.isPresent()) { System.out.println("in if statement"); found = optionalUser.get(); userRepository.deleteById(email); - return found; } - return null; + found.setEmail("not found"); + return found; } } \ No newline at end of file diff --git a/src/main/java/com/example/accessingdatamysql/auth/AuthController.java b/src/main/java/com/example/accessingdatamysql/auth/AuthController.java new file mode 100644 index 0000000000000000000000000000000000000000..83fcb77cc31bed4562405857f35955a7bc0a40d9 --- /dev/null +++ b/src/main/java/com/example/accessingdatamysql/auth/AuthController.java @@ -0,0 +1,76 @@ +package com.example.accessingdatamysql.auth; +import org.springframework.web.bind.annotation.RequestMapping; +import com.example.accessingdatamysql.auth.JWT; + +import io.jsonwebtoken.Claims; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.util.Map; +import java.util.HashMap; +import java.util.Optional; +import javax.print.attribute.standard.Media; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import com.example.accessingdatamysql.UserRepository; +import com.example.accessingdatamysql.User; + +@Controller +@CrossOrigin +@RequestMapping(path="/auth") // This means URL's start with /auth (after Application path) +public class AuthController { + + @Autowired // This means to get the bean called userRepository + // Which is auto-generated by Spring, we will use it to handle the data + private UserRepository userRepository; + @PostMapping(path="/login") + public @ResponseBody Map<String, String> login(@RequestBody Map<String, String> json) + { + // Assuming you have a JSON library for Java, you can use it to build the response + Map<String, String> res = new HashMap<String, String>(); + if (!json.containsKey("email") || !json.containsKey("password")) + { + res.put("result", "bad request"); + return res; + } + Optional<User> user = userRepository.findById(json.get("email")); + if (user.isPresent()) + { + User usr = user.get(); + if (usr.getEmail().equals(json.get("email")) && usr.getPassword().equals(json.get("password"))) + { + res.put("user", user.get().getEmail()); + //give them a token + res.put("jwt", JWT.createJWT("id", "issuer", "sarthaks@vt.edu", 99999999)); + res.put("result", "success"); + return res; + } + res.put("result", "bad password"); + return res; + } + res.put("result", "bad username"); + return res; + } + + @PostMapping(path="/verify") + public @ResponseBody Map<String, String> verify(@RequestBody Map<String, String> json) + { + Map<String, String> res = new HashMap<String, String>(); + if (json.containsKey("jwt")) + { + Claims claim = JWT.decodeJWT(json.get("jwt")); + if (claim != null) + res.put("user", claim.getSubject()); + } + else + { + res.put("login", "failed"); + } + return res; + } +} diff --git a/src/main/java/com/example/accessingdatamysql/auth/JWT.java b/src/main/java/com/example/accessingdatamysql/auth/JWT.java new file mode 100644 index 0000000000000000000000000000000000000000..d2093a89b16cae027e7bc22f660e0a399f93303e --- /dev/null +++ b/src/main/java/com/example/accessingdatamysql/auth/JWT.java @@ -0,0 +1,74 @@ +package com.example.accessingdatamysql.auth; + +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.DatatypeConverter; +import java.security.Key; + +import io.jsonwebtoken.*; + +import java.util.Date; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.Claims; + +/* + A simple static class that is used to create and decode JWTs. + */ +public class JWT{ + + + // The secret key. This should be in a property file NOT under source + // control and not hard coded in real life. We're putting it here for + // simplicity. + private static String SECRET_KEY = "secret dev key"; + private static final long DEFAULT_TTL = 99999; + + //Sample method to construct a JWT + public static String createJWT(String id, String issuer, String subject, long ttlMillis) { + + //The JWT signature algorithm we will be using to sign the token + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + long nowMillis = System.currentTimeMillis(); + Date now = new Date(nowMillis); + + //We will sign our JWT with our ApiKey secret + byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY); + Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); + + //Let's set the JWT Claims + JwtBuilder builder = Jwts.builder().setId(id) + .setIssuedAt(now) + .setSubject(subject) + .setIssuer(issuer) + .signWith(signatureAlgorithm, signingKey); + + //if it has been specified, let's add the expiration + if (ttlMillis >= 0) { + long expMillis = nowMillis + ttlMillis + DEFAULT_TTL; // pad with default amount + Date exp = new Date(expMillis); + builder.setExpiration(exp); + } + + //Builds the JWT and serializes it to a compact, URL-safe string + return builder.compact(); + } + + public static Claims decodeJWT(String jwt) { + + //This line will throw an exception if it is not a signed JWS (as expected) + try + { + Claims claims = Jwts.parser() + .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY)) + .parseClaimsJws(jwt).getBody(); + return claims; + } + catch (Exception e) + { + return null; + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/accessingdatamysql/auth/Token.java b/src/main/java/com/example/accessingdatamysql/auth/Token.java new file mode 100644 index 0000000000000000000000000000000000000000..53b1dd23dc2b5a440790fe499b5d4774858f87ff --- /dev/null +++ b/src/main/java/com/example/accessingdatamysql/auth/Token.java @@ -0,0 +1,5 @@ +package com.example.accessingdatamysql.auth; + +public class Token { + String token; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0855ee3cbfa77172488d42b827b9dfd6d6e0ff83..f791667e1463c0933552a2a6d33e06865c2276c9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ -spring.jpa.hibernate.ddl-auto=create +spring.jpa.hibernate.ddl-auto=none spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/inventory spring.datasource.username=root -spring.datasource.password=CSD@mysql-1872 +spring.datasource.password=czarthak spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #spring.jpa.show-sql: true \ No newline at end of file