From 946c4913fa72cb0be6a2ee7bdf405d5c67348b2b Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Mon, 21 Mar 2022 04:03:20 +1030 Subject: [PATCH] Add Login and Logout routes --- Api/Auth/Login.go | 65 ++++++++++++++++++++++++ Api/Auth/Login_test.go | 111 +++++++++++++++++++++++++++++++++++++++++ Api/Auth/Logout.go | 34 +++++++++++++ Api/Auth/Session.go | 51 +++++++++++++++++++ Api/Routes.go | 6 +++ Api/Users_test.go | 2 +- Database/Users.go | 13 +++++ 7 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 Api/Auth/Login.go create mode 100644 Api/Auth/Login_test.go create mode 100644 Api/Auth/Logout.go create mode 100644 Api/Auth/Session.go diff --git a/Api/Auth/Login.go b/Api/Auth/Login.go new file mode 100644 index 0000000..5d42e5c --- /dev/null +++ b/Api/Auth/Login.go @@ -0,0 +1,65 @@ +package Auth + +import ( + "encoding/json" + "net/http" + "time" + + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" + + "github.com/gofrs/uuid" +) + +type Credentials struct { + Email string `json:"email"` + Password string `json:"password"` +} + +func Login(w http.ResponseWriter, r *http.Request) { + var ( + creds Credentials + userData Models.User + sessionToken uuid.UUID + expiresAt time.Time + err error + ) + + err = json.NewDecoder(r.Body).Decode(&creds) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + userData, err = Database.GetUserByEmail(creds.Email) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if !CheckPasswordHash(creds.Password, userData.Password) { + w.WriteHeader(http.StatusUnauthorized) + return + } + + sessionToken, err = uuid.NewV4() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + expiresAt = time.Now().Add(1 * time.Hour) + + Sessions[sessionToken.String()] = Session{ + Username: userData.Email, + Expiry: expiresAt, + } + + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: sessionToken.String(), + Expires: expiresAt, + }) + + w.WriteHeader(http.StatusOK) +} diff --git a/Api/Auth/Login_test.go b/Api/Auth/Login_test.go new file mode 100644 index 0000000..5861107 --- /dev/null +++ b/Api/Auth/Login_test.go @@ -0,0 +1,111 @@ +package Auth + +import ( + "fmt" + "math/rand" + "net/http" + "net/http/httptest" + "os" + "path" + "runtime" + "strings" + "testing" + "time" + + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database" + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" + + "github.com/gorilla/mux" +) + +var ( + r *mux.Router + letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +) + +func init() { + // Fix working directory for tests + _, filename, _, _ := runtime.Caller(0) + dir := path.Join(path.Dir(filename), "..") + err := os.Chdir(dir) + if err != nil { + panic(err) + } + + Database.InitTest() + + r = mux.NewRouter() +} + +func randString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} + +func createTestUser(random bool) (Models.User, error) { + now := time.Now() + + email := "email@email.com" + if random { + email = fmt.Sprintf("%s@email.com", randString(16)) + } + + password, err := HashPassword("password") + if err != nil { + return Models.User{}, err + } + + userData := Models.User{ + Email: email, + Password: password, + LastLogin: &now, + FirstName: "Hugh", + LastName: "Mann", + } + + err = Database.CreateUser(&userData) + return userData, err +} + +func Test_Login(t *testing.T) { + t.Log("Testing Login...") + + r.HandleFunc("/admin/login", Login).Methods("POST") + + ts := httptest.NewServer(r) + + defer ts.Close() + + userData, err := createTestUser(true) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + t.FailNow() + } + + postJson := ` +{ + "email": "%s", + "password": "password" +} +` + postJson = fmt.Sprintf(postJson, userData.Email) + + res, err := http.Post(ts.URL+"/admin/login", "application/json", strings.NewReader(postJson)) + if err != nil { + t.Errorf("Expected nil, recieved %s", err.Error()) + return + } + + if res.StatusCode != http.StatusOK { + t.Errorf("Expected %d, recieved %d", http.StatusOK, res.StatusCode) + return + } + + if len(res.Cookies()) != 1 { + t.Errorf("Expected cookies len 1, recieved %d", len(res.Cookies())) + return + } +} diff --git a/Api/Auth/Logout.go b/Api/Auth/Logout.go new file mode 100644 index 0000000..822b21d --- /dev/null +++ b/Api/Auth/Logout.go @@ -0,0 +1,34 @@ +package Auth + +import ( + "net/http" + "time" +) + +func Logout(w http.ResponseWriter, r *http.Request) { + var ( + c *http.Cookie + sessionToken string + err error + ) + + c, err = r.Cookie("session_token") + if err != nil { + if err == http.ErrNoCookie { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusBadRequest) + return + } + + sessionToken = c.Value + + delete(Sessions, sessionToken) + + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: "", + Expires: time.Now(), + }) +} diff --git a/Api/Auth/Session.go b/Api/Auth/Session.go new file mode 100644 index 0000000..3e2c23f --- /dev/null +++ b/Api/Auth/Session.go @@ -0,0 +1,51 @@ +package Auth + +import ( + "errors" + "net/http" + "time" +) + +var ( + Sessions = map[string]Session{} +) + +type Session struct { + Username string + Expiry time.Time +} + +func (s Session) IsExpired() bool { + return s.Expiry.Before(time.Now()) +} + +func CheckCookie(r *http.Request) (Session, error) { + var ( + c *http.Cookie + sessionToken string + userSession Session + exists bool + err error + ) + + c, err = r.Cookie("session_token") + if err != nil { + return userSession, err + } + sessionToken = c.Value + + // We then get the session from our session map + userSession, exists = Sessions[sessionToken] + if !exists { + return userSession, errors.New("Cookie not found") + } + + // If the session is present, but has expired, we can delete the session, and return + // an unauthorized status + if userSession.IsExpired() { + delete(Sessions, sessionToken) + return userSession, errors.New("Cookie expired") + } + + return userSession, nil +} diff --git a/Api/Routes.go b/Api/Routes.go index 366d8cd..e29633a 100644 --- a/Api/Routes.go +++ b/Api/Routes.go @@ -3,6 +3,8 @@ package Api import ( "log" + "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Api/Auth" + "github.com/gorilla/mux" ) @@ -34,6 +36,10 @@ func InitApiEndpoints() *mux.Router { router.HandleFunc("/user/{userID}", updatePost).Methods("PUT") router.HandleFunc("/user/{userID}", deletePost).Methods("DELETE") + // Define routes for authentication + router.HandleFunc("/admin/login", Auth.Login).Methods("POST") + router.HandleFunc("/admin/logout", Auth.Logout).Methods("GET") + //router.PathPrefix("/").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir("./uploads")))) return router diff --git a/Api/Users_test.go b/Api/Users_test.go index 162e83f..64cb6e7 100644 --- a/Api/Users_test.go +++ b/Api/Users_test.go @@ -69,7 +69,7 @@ func createTestUser(random bool) (Models.User, error) { } func Test_getUser(t *testing.T) { - t.Log("Testing getPost...") + t.Log("Testing getUser...") r.HandleFunc("/user/{userID}", getUser).Methods("GET") diff --git a/Database/Users.go b/Database/Users.go index 3f9d0a9..0289823 100644 --- a/Database/Users.go +++ b/Database/Users.go @@ -24,6 +24,19 @@ func GetUserById(id string) (Models.User, error) { return userData, err } +func GetUserByEmail(email string) (Models.User, error) { + var ( + userData Models.User + err error + ) + + err = DB.Preload(clause.Associations). + First(&userData, "email = ?", email). + Error + + return userData, err +} + func GetUsers(page, pageSize int) ([]Models.User, error) { var ( users []Models.User