diff --git a/backend/bin/api b/backend/bin/api new file mode 100644 index 0000000..aa5bf83 Binary files /dev/null and b/backend/bin/api differ diff --git a/backend/go.mod b/backend/go.mod index 0741795..5daf080 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -11,4 +11,16 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/labstack/echo/v4 v4.12.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/rs/cors v1.11.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index bf6be7e..600ff94 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -6,7 +6,34 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= diff --git a/backend/main.go b/backend/main.go index b7a86d3..7d1ee6d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -10,6 +10,7 @@ import ( router "github.com/MicrosoftStudentChapter/Link-Generator/pkg/router" "github.com/gorilla/mux" "github.com/redis/go-redis/v9" + "github.com/rs/cors" ) func main() { @@ -31,12 +32,11 @@ func main() { fmt.Println("Redis [PING]: ", res) r := mux.NewRouter() + + // Define routes r.HandleFunc("/links/all", router.GetAllLinks).Methods(http.MethodOptions, http.MethodGet) - r.HandleFunc("/generate-token", auth.GenerateJWT).Methods(http.MethodOptions, http.MethodGet) - r.Handle("/login", auth.TokenRequired(http.HandlerFunc(auth.ProtectedRoute))).Methods(http.MethodOptions, http.MethodGet) - r.HandleFunc("/admin", auth.ProtectedRoute).Methods(http.MethodOptions, http.MethodGet) - r.HandleFunc("/register", auth.Register).Methods(http.MethodOptions, http.MethodPost) - r.HandleFunc("/show/users", auth.ShowUsers).Methods(http.MethodOptions, http.MethodGet) + r.HandleFunc("/login", auth.Login).Methods(http.MethodOptions, http.MethodGet) + r.HandleFunc("/show", auth.ShowUsers).Methods(http.MethodOptions, http.MethodPost) r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("Service is Alive")) @@ -44,21 +44,33 @@ func main() { r.HandleFunc("/add-link", router.AddLink).Methods(http.MethodOptions, http.MethodPost) r.HandleFunc("/{link}", router.HandleRouting).Methods(http.MethodOptions, http.MethodGet) + // Middlewares r.Use(LoggingMiddleware) r.Use(mux.CORSMethodMiddleware(r)) r.Use(HandlePreflight) - fmt.Println("Server started at port 4000") + // Configure CORS + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://localhost:5173"}, // Change this to your front-end URL + AllowCredentials: true, + AllowedMethods: []string{"GET", "POST"}, + AllowedHeaders: []string{"Authorization"}, + }) - http.ListenAndServe(":4000", r) + handler := c.Handler(r) + fmt.Println("Server started at port 4000") + http.ListenAndServe(":4000", handler) } // Middlewares func HandlePreflight(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173") // Change this to your frontend URL + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Credentials", "true") + if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return diff --git a/backend/pkg/auth/auth.go b/backend/pkg/auth/auth.go index 78aee77..ff3ad36 100644 --- a/backend/pkg/auth/auth.go +++ b/backend/pkg/auth/auth.go @@ -8,22 +8,36 @@ import ( "os" "time" - "github.com/dgrijalva/jwt-go" + cookie "github.com/MicrosoftStudentChapter/Link-Generator/pkg/cookies" + + "github.com/golang-jwt/jwt" + "golang.org/x/crypto/bcrypt" ) var jwtKey = []byte(os.Getenv("JWT_SECRET")) var users = map[string]string{} +type User struct { + ID string + Username string + Password string +} + +type Response struct { + Status string `json:"status"` + RedirectUrl string `json:"redirectUrl,omitempty"` + Message string `json:"message,omitempty"` +} + type Claims struct { Username string "json:username" jwt.StandardClaims } -func GenerateJWT(w http.ResponseWriter, r *http.Request) { - username := r.URL.Query().Get("username") +func GenerateTokenAndSetCookies(w http.ResponseWriter, r *http.Request, username string) string { if username == "" { http.Error(w, "Username is required", http.StatusBadRequest) - return + return "" } expirationTime := time.Now().Add(30 * time.Minute) @@ -39,11 +53,12 @@ func GenerateJWT(w http.ResponseWriter, r *http.Request) { tokenString, err := token.SignedString(jwtKey) if err != nil { http.Error(w, "Could not generate token", http.StatusInternalServerError) - return + return "" } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"token": tokenString}) + cookie.SetTokenCookie("access-token", tokenString, expirationTime, w) + + return tokenString } func ValidateJWT(tokenString string) (string, error) { @@ -63,29 +78,47 @@ func ValidateJWT(tokenString string) (string, error) { return claims.Username, nil } -func TokenRequired(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - tokenString := r.Header.Get("Authorization") - if tokenString == "" { - http.Error(w, "Token is missing", http.StatusForbidden) - return - } +func Login(w http.ResponseWriter, r *http.Request) { + username := r.URL.Query().Get("username") + password := r.URL.Query().Get("password") + + var loggedInUser *User - username, err := ValidateJWT(tokenString) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return + users := GetUsers() + + for _, user := range users { + if user.Username != username { + continue + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err == nil { + loggedInUser = user + break } + } - r.Header.Set("username", username) - next.ServeHTTP(w, r) - }) -} + if loggedInUser == nil { + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(Response{Status: "fail", Message: "Invalid Login"}) + return + } + tokenString := GenerateTokenAndSetCookies(w, r, username) -func ProtectedRoute(w http.ResponseWriter, r *http.Request) { - url := "http://localhost:5173/Adminpage" + if tokenString == "" { + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(Response{Status: "fail", Message: "Token is missing"}) + return + } + + _, err := ValidateJWT(tokenString) + if err != nil { + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(Response{Status: "fail", Message: err.Error(), RedirectUrl: "http://localhost:5173/error"}) + return + } + + url := "http://localhost:5173/link-gen" fmt.Printf("Route Url: " + url) - http.Redirect(w, r, url, http.StatusSeeOther) + json.NewEncoder(w).Encode(Response{Status: "success", RedirectUrl: url}) } func Register(w http.ResponseWriter, r *http.Request) { @@ -111,6 +144,28 @@ func Register(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"message": "User registered successfully"}) } +func GetUsers() []*User { + password, _ := bcrypt.GenerateFromPassword([]byte("12345"), 8) + + return []*User{ + { + ID: "1", + Username: "Preet", + Password: string(password), + }, + { + ID: "2", + Username: "Jeevant", + Password: string(password), + }, + { + ID: "3", + Username: "Akshat", + Password: string(password), + }, + } +} + func ShowUsers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(users) diff --git a/backend/pkg/cookies/cookies.go b/backend/pkg/cookies/cookies.go new file mode 100644 index 0000000..62bf6b4 --- /dev/null +++ b/backend/pkg/cookies/cookies.go @@ -0,0 +1,20 @@ +package cookies + +import ( + "net/http" + "time" +) + +func SetTokenCookie(name, token string, expiration time.Time, w http.ResponseWriter) { + cookie := &http.Cookie{ + Name: name, + Value: token, + Expires: expiration, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Secure: false, + } + + http.SetCookie(w, cookie) +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0d2436b..06e3c05 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,7 +16,8 @@ "axios": "^1.6.8", "dayjs": "^1.11.10", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1" }, "devDependencies": { "@types/react": "^18.2.64", @@ -1430,6 +1431,14 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", @@ -4155,6 +4164,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index b668d34..5a20352 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,8 @@ "axios": "^1.6.8", "dayjs": "^1.11.10", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1" }, "devDependencies": { "@types/react": "^18.2.64", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c67064c..b6e21b5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2,7 +2,10 @@ import { Container } from '@mui/material'; import MainContentSection from './Maincontent'; -import "./App.css" +import LoginPageSection from './loginPage'; +import ErrorPageSection from './errorPage'; +import "./App.css"; + const App = () => { return (
@@ -19,10 +22,15 @@ const App = () => { } }} > - + + } /> + } /> + } /> + +
); }; -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/Maincontent.jsx b/frontend/src/Maincontent.jsx index 9f5c8b9..44c79c2 100644 --- a/frontend/src/Maincontent.jsx +++ b/frontend/src/Maincontent.jsx @@ -38,6 +38,39 @@ const MainContentSection = () => { const [noExpiry, setNoExpiry] = useState(false); const [snackbarOpen, setSnackbarOpen] = useState(false); const [error, setError] = useState(""); + + const loginUser = async () => { + const config = { + method: 'GET', + maxBodyLength: Infinity, + url: 'http://localhost:4000/login', + params: { + username: "Preet", + password: "12345", + }, + withCredentials: true + }; + + try { + const response = await axios.request(config); + + if (response.status === 200) { + console.log('Login success'); + const redirectUrl = response.data.redirectUrl; + if (redirectUrl) { + console.log('redirect'); + // window.location.href = redirectUrl; + } else { + console.log('No redirect URL provided by the backend'); + } + } else { + console.log('Login failed'); + } + } catch (error) { + console.error('Error during login:', error.message); + console.error('Full error:', error); + } + }; const handleShortenUrl = async () => { const urlRegex = /^(ftp|http|https):\/\/[^ "]+$/; @@ -49,7 +82,7 @@ const MainContentSection = () => { const shortenedUrl = generateShortenedUrl(alias); - const link = "https://l.mlsctiet.com" + const link = "https://localhost:4000" // api call to add link in the backend const raw = JSON.stringify({ @@ -214,6 +247,11 @@ const MainContentSection = () => { Shorten URL + + + {shortenedUrl && ( Shortened URL:{" "} diff --git a/frontend/src/errorPage.jsx b/frontend/src/errorPage.jsx new file mode 100644 index 0000000..7002a72 --- /dev/null +++ b/frontend/src/errorPage.jsx @@ -0,0 +1,27 @@ +import { useState } from "react"; +import { + TextField, + Button, + Grid, + Typography, +} from "@mui/material"; +import axios from "axios"; + +const ErrorPageSection = () => { + + return ( + + Error Page + + + + ); +}; + +export default ErrorPageSection; \ No newline at end of file diff --git a/frontend/src/loginPage.jsx b/frontend/src/loginPage.jsx new file mode 100644 index 0000000..b539246 --- /dev/null +++ b/frontend/src/loginPage.jsx @@ -0,0 +1,121 @@ +import { useState } from "react"; +import { + TextField, + Button, + Grid, + Typography, +} from "@mui/material"; +import axios from "axios"; + +const LoginPageSection = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + + const registerUser = async () => { + const userData = JSON.stringify({ + user: "Preet", + pass: "12345", + }); + + const config = { + method: "POST", + maxBodyLength: Infinity, + url: "http://localhost:4000/register", + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": + "https://generate.mlsctiet.com/, http://localhost:5173/", + }, + data: userData, + }; + const response = await axios.request(config); + + if (response.status == 200) { + console.log(userData); + console.log("Register success"); + } else { + console.log("Register failed"); + } + } + + const loginUser = async () => { + const config = { + method: 'GET', + maxBodyLength: Infinity, + url: 'http://localhost:4000/login', + params: { + username: username, + password: password, + }, + withCredentials: true + }; + + try { + const response = await axios.request(config); + + if (response.status === 200) { + console.log('Login success'); + const redirectUrl = response.data.redirectUrl; + if (redirectUrl) { + console.log('redirect'); + window.location.href = redirectUrl; + } else { + console.log('No redirect URL provided by the backend'); + } + } else { + console.log('Login failed'); + } + } catch (error) { + console.error('Error during login:', error.message); + console.error('Full error:', error); + } + }; + + return ( + + Sign Up + + + setUsername(e.target.value)} + required + error={!!error} + helperText={error} + /> + + + setPassword(e.target.value)} + /> + + + + + + + + + + ); +}; + +export default LoginPageSection; \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 54b39dd..c7fabfd 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,10 +1,13 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.jsx' -import './index.css' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter as Router } from 'react-router-dom'; +import App from './App.jsx'; +import './index.css'; ReactDOM.createRoot(document.getElementById('root')).render( - - , -) + + + + +); \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 90a80cb..1375e5b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -573,6 +573,11 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@remix-run/router@1.16.1": + version "1.16.1" + resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz" + integrity sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig== + "@rollup/rollup-win32-x64-msvc@4.13.0": version "4.13.0" resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz" @@ -2094,7 +2099,7 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -"react-dom@^17.0.0 || ^18.0.0", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8.0: +"react-dom@^17.0.0 || ^18.0.0", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8, react-dom@>=16.8.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -2122,6 +2127,21 @@ react-refresh@^0.14.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== +react-router-dom@^6.23.1: + version "6.23.1" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz" + integrity sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ== + dependencies: + "@remix-run/router" "1.16.1" + react-router "6.23.1" + +react-router@6.23.1: + version "6.23.1" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz" + integrity sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ== + dependencies: + "@remix-run/router" "1.16.1" + react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" @@ -2132,7 +2152,7 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -"react@^17.0.0 || ^18.0.0", react@^18.2.0, react@>=16.6.0, react@>=16.8.0: +"react@^17.0.0 || ^18.0.0", react@^18.2.0, react@>=16.6.0, react@>=16.8, react@>=16.8.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==