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 (