From 959b4a8dd2c80cf306b1fd55846a9611295f825b Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 31 Jul 2025 11:36:49 -0400 Subject: [PATCH 1/4] Created auth0 login and signup --- .env.local | 3 + package-lock.json | 10 +- package.json | 2 +- src/App.jsx | 96 ++++++++++++++++---- src/auth0-config.js | 9 ++ src/components/CSS/AuthStyles.css | 146 ++++++++++++++++++++++++++++++ src/components/Login.jsx | 69 ++++++++++++-- src/components/Signup.jsx | 91 +++++++++++++++---- src/shared.js | 2 +- webpack.config.js | 13 ++- 10 files changed, 388 insertions(+), 53 deletions(-) create mode 100644 .env.local create mode 100644 src/auth0-config.js create mode 100644 src/components/CSS/AuthStyles.css diff --git a/.env.local b/.env.local new file mode 100644 index 00000000..e47dfc05 --- /dev/null +++ b/.env.local @@ -0,0 +1,3 @@ +VITE_AUTH0_DOMAIN=franccescopetta.us.auth0.com +VITE_AUTH0_CLIENT_ID=h1SYjGM6qWwIZMRTOSI7yjdjEzp3iAkS +PORT=3000 diff --git a/package-lock.json b/package-lock.json index dc268f09..db8ff7a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@auth0/auth0-react": "^2.3.0", + "@auth0/auth0-react": "^2.4.0", "@babel/core": "^7.27.4", "@babel/preset-react": "^7.27.1", "axios": "^1.10.0", @@ -45,12 +45,12 @@ } }, "node_modules/@auth0/auth0-react": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-2.3.0.tgz", - "integrity": "sha512-YYTc/DWWigKC9fURufR/79h3+3DAnIzbfEzJLZ8Z4Q0BXE0azru3pKUbU+vYzS4lMAJkclwLuAbUnLjK81vCpA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@auth0/auth0-react/-/auth0-react-2.4.0.tgz", + "integrity": "sha512-5bt3sO9FVupNM15IpqyYu/2OPHpLI5El7RgWLQXZOPbnCBbtl+VgdHR+H2NfhNQ4SqQtC/5uKbHWafcVcsxkiw==", "license": "MIT", "dependencies": { - "@auth0/auth0-spa-js": "^2.1.3" + "@auth0/auth0-spa-js": "^2.2.0" }, "peerDependencies": { "react": "^16.11.0 || ^17 || ^18 || ^19", diff --git a/package.json b/package.json index da1486c6..83ce0264 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "license": "ISC", "description": "", "dependencies": { - "@auth0/auth0-react": "^2.3.0", + "@auth0/auth0-react": "^2.4.0", "@babel/core": "^7.27.4", "@babel/preset-react": "^7.27.1", "axios": "^1.10.0", diff --git a/src/App.jsx b/src/App.jsx index d793b9af..2722cc3e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,23 +3,36 @@ import { createRoot } from "react-dom/client"; import axios from "axios"; import "./AppStyles.css"; import NavBar from "./components/NavBar"; -import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; import Login from "./components/Login"; import Signup from "./components/Signup"; import Home from "./components/Home"; import NotFound from "./components/NotFound"; import { API_URL } from "./shared"; +import { Auth0Provider, useAuth0 } from "@auth0/auth0-react"; +import { auth0Config } from "./auth0-config"; const App = () => { const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const navigate = useNavigate(); + + const { + isAuthenticated, + isLoading: auth0Loading, + user: auth0User, + logout: auth0Logout, + } = useAuth0(); const checkAuth = async () => { try { const response = await axios.get(`${API_URL}/auth/me`, { withCredentials: true, }); - setUser(response.data.user); - } catch { + if (response.data.user) { + setUser(response.data.user); + } + } catch (error) { console.log("Not authenticated"); setUser(null); } @@ -27,25 +40,74 @@ const App = () => { // Check authentication status on app load useEffect(() => { - checkAuth(); - }, []); + const initAuth = async () => { + if (!auth0Loading) { + if (isAuthenticated && auth0User) { + await handleAuth0Login(); + } else { + await checkAuth(); + } + setLoading(false); + } + }; + initAuth(); + }, [isAuthenticated, auth0User, auth0Loading]); - const handleLogout = async () => { + const handleAuth0Login = async () => { try { - // Logout from our backend - await axios.post( - `${API_URL}/auth/logout`, - {}, + setLoading(true); + const response = await axios.post( + `${API_URL}/auth/auth0`, + { + auth0Id: auth0User.sub, + email: auth0User.email, + username: auth0User.nickname || auth0User.email?.split("@")[0], + }, { withCredentials: true, } ); - setUser(null); + + if (response.data.token) { + localStorage.setItem('token', response.data.token); + } + + setUser(response.data.user); + navigate("/"); + } catch (error) { + console.error("Auth0 login error:", error); + } finally { + setLoading(false); + } + }; + + const handleLogout = async () => { + try { + await axios.post(`${API_URL}/auth/logout`, {}, { + withCredentials: true, + }); } catch (error) { console.error("Logout error:", error); + } finally { + localStorage.removeItem('token'); + setUser(null); + + if (isAuthenticated) { + auth0Logout({ + logoutParams: { + returnTo: window.location.origin, + }, + }); + } else { + navigate("/"); + } } }; + if (loading || auth0Loading) { + return
Loading...
; + } + return (
@@ -53,7 +115,7 @@ const App = () => { } /> } /> - } /> + } /> } />
@@ -63,11 +125,13 @@ const App = () => { const Root = () => { return ( - - - + + + + + ); }; const root = createRoot(document.getElementById("root")); -root.render(); +root.render(); \ No newline at end of file diff --git a/src/auth0-config.js b/src/auth0-config.js new file mode 100644 index 00000000..8fb1e6a2 --- /dev/null +++ b/src/auth0-config.js @@ -0,0 +1,9 @@ +export const auth0Config = { + domain: process.env.REACT_APP_AUTH0_DOMAIN || "franccescopetta.us.auth0.com", + clientId: process.env.REACT_APP_AUTH0_CLIENT_ID || "h1SYjGM6qWwIZMRTOSI7yjdjEzp3iAkS", + authorizationParams: { + redirect_uri: `${window.location.origin}/login`, + audience: process.env.REACT_APP_AUTH0_AUDIENCE, + scope: "openid profile email", + }, +}; \ No newline at end of file diff --git a/src/components/CSS/AuthStyles.css b/src/components/CSS/AuthStyles.css new file mode 100644 index 00000000..bd96886f --- /dev/null +++ b/src/components/CSS/AuthStyles.css @@ -0,0 +1,146 @@ +.auth-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; + padding: 20px; +} + +.auth-form { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 400px; +} + +.auth-form h2 { + text-align: center; + margin-bottom: 1.5rem; + color: #333; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #555; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + transition: border-color 0.2s; +} + +.form-group input:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); +} + +.form-group input.error { + border-color: #dc3545; +} + +.error-text { + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.25rem; + display: block; +} + +.error-message { + background-color: #f8d7da; + color: #721c24; + padding: 0.75rem; + border-radius: 4px; + margin-bottom: 1rem; + border: 1px solid #f5c6cb; +} + +.auth-form button { + width: 100%; + padding: 0.75rem; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.auth-form button:hover:not(:disabled) { + background-color: #0056b3; +} + +.auth-form button:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +.auth-divider { + text-align: center; + margin: 1.5rem 0; + position: relative; +} + +.auth-divider::before { + content: ""; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background-color: #ddd; +} + +.auth-divider span { + background-color: white; + padding: 0 1rem; + color: #666; + font-size: 0.875rem; +} + +.auth0-login-btn { + width: 100%; + padding: 0.75rem; + background-color: #eb5424; + color: white; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + margin-bottom: 1rem; +} + +.auth0-login-btn:hover { + background-color: #d4451d; +} + +.auth-link { + text-align: center; + margin-top: 1rem; + color: #666; +} + +.auth-link a { + color: #007bff; + text-decoration: none; +} + +.auth-link a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/src/components/Login.jsx b/src/components/Login.jsx index 849e495a..4d853396 100644 --- a/src/components/Login.jsx +++ b/src/components/Login.jsx @@ -1,8 +1,10 @@ -import React, { useState } from "react"; -import { useNavigate, Link } from "react-router-dom"; +import React, { useState, useEffect } from "react"; +import { useNavigate, Link, useLocation } from "react-router-dom"; +import { useAuth0 } from "@auth0/auth0-react"; import axios from "axios"; import { API_URL } from "../shared"; -import "./AuthStyles.css"; +import "./CSS/AuthStyles.css"; +import { auth0Config } from "../auth0-config"; const Login = ({ setUser }) => { const [formData, setFormData] = useState({ @@ -11,7 +13,23 @@ const Login = ({ setUser }) => { }); const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); const navigate = useNavigate(); + const location = useLocation(); + const { loginWithRedirect, isAuthenticated, user: auth0User, isLoading: auth0Loading } = useAuth0(); + + useEffect(() =>{ + if(!auth0Loading && isAuthenticated && auth0User){ + navigate("/"); + } + }, [isAuthenticated, auth0User, auth0Loading, navigate]); + + useEffect(() => { + if (location.state?.message) { + setSuccessMessage(location.state.message); + window.history.replaceState({}, document.title); + } + }, [location]); const validateForm = () => { const newErrors = {}; @@ -40,14 +58,22 @@ const Login = ({ setUser }) => { } setIsLoading(true); + setSuccessMessage(""); + try { const response = await axios.post(`${API_URL}/auth/login`, formData, { withCredentials: true, }); + if (response.data.token) { + localStorage.setItem('token', response.data.token); + } + setUser(response.data.user); + navigate("/"); } catch (error) { + console.error("Login error:", error); if (error.response?.data?.error) { setErrors({ general: error.response.data.error }); } else { @@ -65,7 +91,6 @@ const Login = ({ setUser }) => { [name]: value, })); - // Clear error when user starts typing if (errors[name]) { setErrors((prev) => ({ ...prev, @@ -74,11 +99,28 @@ const Login = ({ setUser }) => { } }; + const handleAuth0Login = () => { + loginWithRedirect(); + }; + + if (auth0Loading) { + return ( +
+
Loading...
+
+ ); + } + + return (

Login

+ {successMessage && ( +
{successMessage}
+ )} + {errors.general && (
{errors.general}
)} @@ -93,6 +135,7 @@ const Login = ({ setUser }) => { value={formData.username} onChange={handleChange} className={errors.username ? "error" : ""} + placeholder="Enter your username" /> {errors.username && ( {errors.username} @@ -108,17 +151,31 @@ const Login = ({ setUser }) => { value={formData.password} onChange={handleChange} className={errors.password ? "error" : ""} + placeholder="Enter your password" /> {errors.password && ( {errors.password} )}
- +
+ or +
+ + +

Don't have an account? Sign up

@@ -127,4 +184,4 @@ const Login = ({ setUser }) => { ); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/src/components/Signup.jsx b/src/components/Signup.jsx index 989fa096..e31d500f 100644 --- a/src/components/Signup.jsx +++ b/src/components/Signup.jsx @@ -1,10 +1,11 @@ import React, { useState } from "react"; import { useNavigate, Link } from "react-router-dom"; +import { useAuth0 } from "@auth0/auth0-react"; import axios from "axios"; -import "./AuthStyles.css"; import { API_URL } from "../shared"; +import "./CSS/AuthStyles.css"; -const Signup = ({ setUser }) => { +const Signup = ({ setUser, onAuth0Login }) => { const [formData, setFormData] = useState({ username: "", password: "", @@ -14,6 +15,8 @@ const Signup = ({ setUser }) => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); + const { loginWithRedirect } = useAuth0(); + const validateForm = () => { const newErrors = {}; @@ -21,6 +24,8 @@ const Signup = ({ setUser }) => { newErrors.username = "Username is required"; } else if (formData.username.length < 3 || formData.username.length > 20) { newErrors.username = "Username must be between 3 and 20 characters"; + } else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) { + newErrors.username = "Username can only contain letters, numbers, and underscores"; } if (!formData.password) { @@ -47,23 +52,39 @@ const Signup = ({ setUser }) => { } setIsLoading(true); + setErrors({}); + try { - const response = await axios.post( - `${API_URL}/auth/signup`, - { - username: formData.username, - password: formData.password, - }, - { withCredentials: true } - ); - - setUser(response.data.user); - navigate("/"); + const response = await axios.post(`${API_URL}/auth/signup`, { + username: formData.username, + password: formData.password, + }, { + withCredentials: true, + }); + + console.log("Signup successful:", response.data); + + if (response.data.token) { + localStorage.setItem('token', response.data.token); + } + + navigate("/login", { + state: { + message: "Account created successfully! Please log in with your new credentials." + } + }); + } catch (error) { - if (error.response?.data?.error) { + console.error("Signup error:", error); + + if (error.response?.status === 409) { + setErrors({ username: "This username is already taken. Please choose a different one." }); + } else if (error.response?.data?.error) { setErrors({ general: error.response.data.error }); + } else if (error.response?.status === 400) { + setErrors({ general: "Please check your input and try again." }); } else { - setErrors({ general: "An error occurred during signup" }); + setErrors({ general: "An error occurred during signup. Please try again." }); } } finally { setIsLoading(false); @@ -77,13 +98,27 @@ const Signup = ({ setUser }) => { [name]: value, })); - // Clear error when user starts typing if (errors[name]) { setErrors((prev) => ({ ...prev, [name]: "", })); } + + if (errors.general) { + setErrors((prev) => ({ + ...prev, + general: "", + })); + } + }; + + const handleAuth0Signup = () => { + loginWithRedirect({ + authorizationParams: { + screen_hint: "signup", + }, + }); }; return ( @@ -105,6 +140,7 @@ const Signup = ({ setUser }) => { value={formData.username} onChange={handleChange} className={errors.username ? "error" : ""} + placeholder="Choose a username (3-20 characters)" /> {errors.username && ( {errors.username} @@ -120,6 +156,7 @@ const Signup = ({ setUser }) => { value={formData.password} onChange={handleChange} className={errors.password ? "error" : ""} + placeholder="Enter a password (minimum 6 characters)" /> {errors.password && ( {errors.password} @@ -135,23 +172,37 @@ const Signup = ({ setUser }) => { value={formData.confirmPassword} onChange={handleChange} className={errors.confirmPassword ? "error" : ""} + placeholder="Confirm your password" /> {errors.confirmPassword && ( {errors.confirmPassword} )}
- +
+ or +
+ + +

- Already have an account? Login + Already have an account? Log in

); }; -export default Signup; +export default Signup; \ No newline at end of file diff --git a/src/shared.js b/src/shared.js index 818db4f2..02286bd7 100644 --- a/src/shared.js +++ b/src/shared.js @@ -1 +1 @@ -export const API_URL = process.env.API_URL || "http://localhost:8080"; +export const API_URL = process.env.API_URL || "https://capstone-2-backend-three.vercel.app/"; diff --git a/webpack.config.js b/webpack.config.js index bfa8e19e..e3fc840a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,10 +13,15 @@ module.exports = { devtool: "source-map", plugins: [ new webpack.EnvironmentPlugin({ - API_URL: "http://localhost:8080", - REACT_APP_AUTH0_DOMAIN: "", - REACT_APP_AUTH0_CLIENT_ID: "", - REACT_APP_AUTH0_AUDIENCE: "", + API_URL: + process.env.API_URL || " https://capstone-2-backend-three.vercel.app", + REACT_APP_AUTH0_DOMAIN: + process.env.REACT_APP_AUTH0_DOMAIN || "franccescopetta.us.auth0.com", + REACT_APP_AUTH0_CLIENT_ID: + process.env.REACT_APP_AUTH0_CLIENT_ID, + REACT_APP_AUTH0_AUDIENCE: + process.env.REACT_APP_AUTH0_AUDIENCE || + "https://franccescopetta.us.auth0.com/api/v2/", }), ], module: { From 1d8453ac3e3ba7cd544e3831980c6a40a8e590a1 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 31 Jul 2025 11:54:25 -0400 Subject: [PATCH 2/4] Hot fix env variables --- .env.local | 3 --- webpack.config.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .env.local diff --git a/.env.local b/.env.local deleted file mode 100644 index e47dfc05..00000000 --- a/.env.local +++ /dev/null @@ -1,3 +0,0 @@ -VITE_AUTH0_DOMAIN=franccescopetta.us.auth0.com -VITE_AUTH0_CLIENT_ID=h1SYjGM6qWwIZMRTOSI7yjdjEzp3iAkS -PORT=3000 diff --git a/webpack.config.js b/webpack.config.js index e3fc840a..d3d5e19f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,7 @@ module.exports = { plugins: [ new webpack.EnvironmentPlugin({ API_URL: - process.env.API_URL || " https://capstone-2-backend-three.vercel.app", + process.env.API_URL || "https://capstone-2-backend-three.vercel.app", REACT_APP_AUTH0_DOMAIN: process.env.REACT_APP_AUTH0_DOMAIN || "franccescopetta.us.auth0.com", REACT_APP_AUTH0_CLIENT_ID: From 89a1bff4bcf6641516b0a009825774a63779906d Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 31 Jul 2025 11:59:01 -0400 Subject: [PATCH 3/4] . --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index d3d5e19f..6130df12 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,7 +18,7 @@ module.exports = { REACT_APP_AUTH0_DOMAIN: process.env.REACT_APP_AUTH0_DOMAIN || "franccescopetta.us.auth0.com", REACT_APP_AUTH0_CLIENT_ID: - process.env.REACT_APP_AUTH0_CLIENT_ID, + process.env.REACT_APP_AUTH0_CLIENT_ID || "h1SYjGM6qWwIZMRTOSI7yjdjEzp3iAkS", REACT_APP_AUTH0_AUDIENCE: process.env.REACT_APP_AUTH0_AUDIENCE || "https://franccescopetta.us.auth0.com/api/v2/", From 1eb1090088a9e77abf20523a5891c248dd332764 Mon Sep 17 00:00:00 2001 From: Franccesco Petta Date: Thu, 31 Jul 2025 14:23:05 -0400 Subject: [PATCH 4/4] Will remove --- dist/index.html | 1 + src/App.jsx | 13 ++- src/components/NavBar.jsx | 5 +- src/components/SpotifyCallback.jsx | 56 +++++++++++ src/components/SpotifyConnect.jsx | 144 +++++++++++++++++++++++++++++ src/shared.js | 2 +- 6 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 src/components/SpotifyCallback.jsx create mode 100644 src/components/SpotifyConnect.jsx diff --git a/dist/index.html b/dist/index.html index e864e00a..f4a2a8b4 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4,6 +4,7 @@ + Capstone 1 diff --git a/src/App.jsx b/src/App.jsx index 2722cc3e..86f593ed 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,15 +2,18 @@ import React, { useState, useEffect } from "react"; import { createRoot } from "react-dom/client"; import axios from "axios"; import "./AppStyles.css"; -import NavBar from "./components/NavBar"; import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; +import { API_URL } from "./shared"; +import { Auth0Provider, useAuth0 } from "@auth0/auth0-react"; +import { auth0Config } from "./auth0-config"; + +import NavBar from "./components/NavBar"; import Login from "./components/Login"; import Signup from "./components/Signup"; import Home from "./components/Home"; import NotFound from "./components/NotFound"; -import { API_URL } from "./shared"; -import { Auth0Provider, useAuth0 } from "@auth0/auth0-react"; -import { auth0Config } from "./auth0-config"; +import SpotifyConnect from "./components/SpotifyConnect"; +import SpotifyCallback from "./components/SpotifyCallback"; const App = () => { const [user, setUser] = useState(null); @@ -116,6 +119,8 @@ const App = () => { } /> } /> } /> + } /> + } /> } /> diff --git a/src/components/NavBar.jsx b/src/components/NavBar.jsx index 648f3808..d2f7da29 100644 --- a/src/components/NavBar.jsx +++ b/src/components/NavBar.jsx @@ -12,6 +12,9 @@ const NavBar = ({ user, onLogout }) => {
{user ? (
+ + Spotify + Welcome, {user.username}! +
+ ) : ( +
+
+

Connected as: {spotifyData.profile?.display_name}

+ {spotifyData.profile?.images?.[0] && ( + Profile + )} +
+ +
+ + +
+ + {topTracks.length > 0 && ( +
+

Your Top Tracks

+ {topTracks.map((track, index) => ( +
+ {index + 1}. + {track.name} by{" "} + {track.artists.map((a) => a.name).join(", ")} +
+ ))} +
+ )} +
+ )} +
+ ); +}; + +export default SpotifyConnect; diff --git a/src/shared.js b/src/shared.js index 02286bd7..2d0f6045 100644 --- a/src/shared.js +++ b/src/shared.js @@ -1 +1 @@ -export const API_URL = process.env.API_URL || "https://capstone-2-backend-three.vercel.app/"; +export const API_URL = process.env.API_URL || "https://capstone-2-backend-three.vercel.app";