From 5fa0de01a6a894220d51b54f54375c28ce4913f6 Mon Sep 17 00:00:00 2001 From: Sarthak Shrivastava <sarthaks@vt.edu> Date: Fri, 24 Nov 2023 22:50:13 -0500 Subject: [PATCH] work in progress auth, login, routing but it builds :) --- .../components/{useToken.js => useToken.jsx} | 17 ++- .../components/user/AccountInformation.css | 38 ++++++ .../components/user/AccountInformation.jsx | 123 ++++++++++++++++++ .../src/components/user/DeleteUser.jsx | 15 +-- .../src/components/user/Login.css | 44 +++++++ .../src/components/user/Login.jsx | 43 ++++-- pom.xml | 17 ++- .../accessingdatamysql/MainController.java | 24 +++- .../auth/AuthController.java | 25 +++- .../example/accessingdatamysql/auth/JWT.java | 74 +++++++++++ 10 files changed, 390 insertions(+), 30 deletions(-) rename inventory-manager/src/components/{useToken.js => useToken.jsx} (56%) create mode 100644 inventory-manager/src/components/user/AccountInformation.css create mode 100644 inventory-manager/src/components/user/AccountInformation.jsx create mode 100644 inventory-manager/src/components/user/Login.css create mode 100644 src/main/java/com/example/accessingdatamysql/auth/JWT.java diff --git a/inventory-manager/src/components/useToken.js b/inventory-manager/src/components/useToken.jsx similarity index 56% rename from inventory-manager/src/components/useToken.js rename to inventory-manager/src/components/useToken.jsx index b15cd6a..5c0106a 100644 --- a/inventory-manager/src/components/useToken.js +++ b/inventory-manager/src/components/useToken.jsx @@ -1,13 +1,24 @@ 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); - if (userToken.user != null) - return 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; }; @@ -15,7 +26,7 @@ export default function useToken() { const saveToken = userToken => { sessionStorage.setItem('token', JSON.stringify(userToken.data)); - setToken(userToken.token); + setToken(userToken.data); }; return { diff --git a/inventory-manager/src/components/user/AccountInformation.css b/inventory-manager/src/components/user/AccountInformation.css new file mode 100644 index 0000000..ce3388d --- /dev/null +++ b/inventory-manager/src/components/user/AccountInformation.css @@ -0,0 +1,38 @@ +/* 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; +} diff --git a/inventory-manager/src/components/user/AccountInformation.jsx b/inventory-manager/src/components/user/AccountInformation.jsx new file mode 100644 index 0000000..7884328 --- /dev/null +++ b/inventory-manager/src/components/user/AccountInformation.jsx @@ -0,0 +1,123 @@ +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: "", + }); + + 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); + 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> + <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 75c1de1..3121abb 100644 --- a/inventory-manager/src/components/user/DeleteUser.jsx +++ b/inventory-manager/src/components/user/DeleteUser.jsx @@ -4,17 +4,16 @@ import useToken from "../useToken"; export const DeleteUser = (token) => { const [email, setEmail] = useState(""); - console.log(JSON.parse(token.token).user); + // 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: JSON.parse(token.token).user - } + Axios.post("http://localhost:8080/user/delete", { + jwt: token.token.jwt }).then((response) => { - console.log(response); + // console.log(response); + // if (response.data.email == "") }); }; diff --git a/inventory-manager/src/components/user/Login.css b/inventory-manager/src/components/user/Login.css new file mode 100644 index 0000000..330234b --- /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 f80c507..440067e 100644 --- a/inventory-manager/src/components/user/Login.jsx +++ b/inventory-manager/src/components/user/Login.jsx @@ -1,21 +1,39 @@ import React, { useState } from "react"; import Axios from "axios"; import PropTypes from "prop-types"; -export const Login = ({setToken}) => { +import './Login.css'; // Import your external CSS file + +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(typeof(response)); - setToken(response); - console.log(response.data); - }); + console.log(response); + // console.log(response.data.login); + if (response.data.login != "bad password") + { + 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 ( @@ -40,12 +58,19 @@ export const Login = ({setToken}) => { 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>} </form> </div> ); -}; +}; + Login.propTypes = { setToken: PropTypes.func.isRequired }; + export default Login; diff --git a/pom.xml b/pom.xml index 6370c92..a030a6c 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,22 @@ <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 9e22665..d21ae88 100644 --- a/src/main/java/com/example/accessingdatamysql/MainController.java +++ b/src/main/java/com/example/accessingdatamysql/MainController.java @@ -7,6 +7,9 @@ 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 @@ -67,21 +70,28 @@ public class MainController { return userRepository.findById(email); } - @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 index 657ca54..fe30cec 100644 --- a/src/main/java/com/example/accessingdatamysql/auth/AuthController.java +++ b/src/main/java/com/example/accessingdatamysql/auth/AuthController.java @@ -1,6 +1,8 @@ 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; @@ -43,12 +45,31 @@ public class AuthController { 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)); return res; } - res.put("login", "failed"); + res.put("login", "bad password"); return res; } - res.put("login", "failed"); + res.put("login", "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 0000000..d2093a8 --- /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 -- GitLab