diff --git a/README.md b/README.md index b8e119c..beee391 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,24 @@ # ThePlaceHolders -"UTSA Place" Collaborate website canvas that allows students to place a pixel within a restrcited amount of time to make art. +"UTSA Place" Collaborate website canvas that allows students to place a pixel +within a restricted amount of time to make art. -Users Connected (testing if working): -Adan Santos -Jackson Sovilay -Hello -Alex Rivera -Hello -Testing -Test push \ No newline at end of file +## How to run +1. Install [Go](https://go.dev/dl/) +2. Run the command `go run .` inside this folder +3. View the website at [127.0.0.1:8080](http://127.0.0.1:8080/) + +## Go project structure +* [go.mod](go.mod) Go version and library dependencies +* [go.sum](go.sum) Checksums for libraries + +## Backend Source +Our back-end is written in [Go](https://go.dev/) using the standard library. +* [Request handling](server.go) +* [User registration/login](users.go) + +## Frontend Source +Our front-end is written in vanilla JavaScript, using the [Bootstrap](https://getbootstrap.com/) +CSS framework. +* [Main page](static/index.html) +* [Login page](static/login.html) +* [Registration page](static/register.html) diff --git a/ThePlaceHolders b/ThePlaceHolders deleted file mode 100755 index d394606..0000000 Binary files a/ThePlaceHolders and /dev/null differ diff --git a/go.mod b/go.mod index 8442ce1..55c3846 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/adanrsantos/ThePlaceHolders -go 1.23.1 +go 1.23 + +require ( + github.com/gorilla/sessions v1.4.0 + golang.org/x/crypto v0.27.0 +) + +require github.com/gorilla/securecookie v1.1.2 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b04990b --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= diff --git a/server.go b/server.go index 408183e..3a89495 100644 --- a/server.go +++ b/server.go @@ -2,57 +2,48 @@ package main import ( "fmt" + "log" "net/http" + + "github.com/gorilla/sessions" ) const ADDRESS = "127.0.0.1" const PORT = "8080" -type UserForm struct { - Email string - Password string -} - -func extract_user_data(r *http.Request) UserForm { - return UserForm{ - Email: r.FormValue("email"), - Password: r.FormValue("password"), - } -} - -func handle_login(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - return - } - data := extract_user_data(r) - fmt.Fprintln(w, data.Email) - fmt.Fprintln(w, data.Password) -} - -func handle_register(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - return - } - data := extract_user_data(r) - fmt.Fprintln(w, data.Email) - fmt.Fprintln(w, data.Password) +type Server struct { + // Registered user information + Users map[string]UserData + // Login sessions + Sessions *sessions.CookieStore } func main() { + // Create server object + secret := []byte("super-secret-key") + server := Server{ + Users: make(map[string]UserData), + Sessions: sessions.NewCookieStore(secret), + } // Host static files static_files := http.FileServer(http.Dir("static/")) http.Handle("/", static_files) - - // Response generated by code - http.HandleFunc("/handle-register", handle_register) - http.HandleFunc("/handle-login", handle_login) - + // Redirect .html to clean URL + http.Handle("/register.html", http.RedirectHandler("/register", 301)) + http.Handle("/login.html", http.RedirectHandler("/login", 301)) + // Handle user authentication + http.HandleFunc("/register", server.handle_register) + http.HandleFunc("/login", server.handle_login) + http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { + server.handle_logout(w, r) + http.Redirect(w, r, "/", http.StatusFound) + }) // Start web server at 127.0.0.1:8080 - e := http.ListenAndServe(ADDRESS+":"+PORT, nil) + fmt.Printf("Listening to %s on port %s...\n", ADDRESS, PORT) + err := http.ListenAndServe(ADDRESS+":"+PORT, nil) // Print any errors - if e != nil { - fmt.Println(e) - } else { - fmt.Println("Started server successfully") + if err != nil { + fmt.Println("Error starting server:") + log.Fatal(err) } } diff --git a/static/index.html b/static/index.html index 823624b..af83bfb 100644 --- a/static/index.html +++ b/static/index.html @@ -14,11 +14,14 @@ diff --git a/static/login.html b/static/login.html index 6f9bd8e..1446f54 100644 --- a/static/login.html +++ b/static/login.html @@ -9,5 +9,26 @@ +
+
+
+ + +
We'll never share your email with anyone else.
+
+
+ + +
+
+ + +
+ +
+ Don't have an account? Register here. +
+
+
diff --git a/static/register.html b/static/register.html index fe4a8df..a67eab6 100644 --- a/static/register.html +++ b/static/register.html @@ -10,15 +10,20 @@
-
+
+<<<<<<< HEAD +======= + + +>>>>>>> c160c57da5e2a70b406ce21fd0d3b48efdc62b36
We'll never share your email with anyone else.
- - + +
diff --git a/users.go b/users.go new file mode 100644 index 0000000..ea0834d --- /dev/null +++ b/users.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "net/http" + "regexp" + "strings" + "time" + + "golang.org/x/crypto/bcrypt" +) + +const SESSION_COOKIE_NAME = "utsa-place-session" +const SESSION_AUTH = "auth" +const SESSION_STARTED = "age" + +const ENCRYPTION_STRENGTH = 14 + +type UserData struct { + Email string + Password string + AccountCreated time.Time + LastLogin time.Time +} + +func validate_email(email string) (string, bool) { + email = strings.ToLower(email) + regex := regexp.MustCompile("^[a-z]+.[a-z]+@(my.)?utsa.edu") + ok := regex.MatchString(email) + return email, ok +} + +// Encrypts a password +func hash_password(password string) string { + bytes, _ := bcrypt.GenerateFromPassword([]byte(password), ENCRYPTION_STRENGTH) + return string(bytes) +} + +// Compares an unencrpyted password to an encrypted password +func check_password_hash(password string, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// Handles requests to /login.html +func (s *Server) handle_login(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + http.ServeFile(w, r, "./static/login.html") + case http.MethodPost: + // Get data from form + email := r.FormValue("email") + password := r.FormValue("password") + // Get user from database + user, ok := s.Users[email] + // If user does not exist + if !ok { + http.Error(w, "User not found", http.StatusForbidden) + return + } + // If password does not match + if !check_password_hash(password, user.Password) { + http.Error(w, "Passwords dont match", http.StatusForbidden) + return + } + // Generate session + session, err := s.Sessions.Get(r, SESSION_COOKIE_NAME) + if err != nil { + s.handle_logout(w, r) + http.Error(w, "Invalid session", http.StatusUnauthorized) + return + } + now := time.Now() + session.Values[SESSION_AUTH] = true + session.Values[SESSION_STARTED] = now + session.Save(r, w) + // Update last-login on DB + user.LastLogin = now + s.Users[email] = user + // Redirect to index.html + fmt.Println("Logged in user: ", email) + http.Redirect(w, r, "/", http.StatusFound) + default: + http.Error(w, "Forbidden", http.StatusForbidden) + } +} + +// Handles requests to /register.html +func (s *Server) handle_register(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + http.ServeFile(w, r, "./static/register.html") + case http.MethodPost: + // Get data from form + email, ok := validate_email(r.FormValue("email")) + if !ok { + http.Error(w, "Invalid email address", http.StatusForbidden) + return + } + password := r.FormValue("password") + if len(password) < 8 || len(password) >= 70 { + http.Error(w, "Invalid password length", http.StatusForbidden) + return + } + // Check that this email is not already registered + if _, ok := s.Users[email]; ok { + http.Error(w, "Already registered", http.StatusForbidden) + return + } + // Generate session + session, err := s.Sessions.Get(r, SESSION_COOKIE_NAME) + // If session cookie invalid + if err != nil { + s.handle_logout(w, r) + http.Error(w, "Invalid session", http.StatusUnauthorized) + return + } + now := time.Now() + // Save user information to DB + s.Users[email] = UserData{ + Email: email, + Password: hash_password(password), + AccountCreated: now, + LastLogin: now, + } + // Make session valid + session.Values[SESSION_AUTH] = true + session.Values[SESSION_STARTED] = now + // Send session token to browser + session.Save(r, w) + // Redirect to index.html + fmt.Println("Registered user: ", email) + http.Redirect(w, r, "/", http.StatusFound) + default: + http.Error(w, "Forbidden", http.StatusForbidden) + } +} + +func (s *Server) handle_logout(w http.ResponseWriter, r *http.Request) { + // If session exists + if session, err := s.Sessions.Get(r, SESSION_COOKIE_NAME); err == nil { + // Remove authorization + session.Values[SESSION_AUTH] = false + session.Save(r, w) + } + // Remove session cookie + http.SetCookie(w, &http.Cookie{ + Name: SESSION_COOKIE_NAME, + // Negative max age immediately removes the cookie + MaxAge: -1, + }) +}