Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 117 additions & 43 deletions internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,26 @@ type IndexPageData struct {
BasePageData
}

type SignupForm struct {
Email string
Password string
Valid bool
}

type SignupPageData struct {
Title string
SignupDetails
SignupForm
}

type LoginForm struct {
Email string
Password string
Valid bool
}

type LoginPageData struct {
Title string
LoginForm
}

type AttributionsPageData struct {
Expand Down Expand Up @@ -82,22 +99,16 @@ func (a *APIConfig) HandleSignupPage(w http.ResponseWriter, r *http.Request) {
}
}

type SignupDetails struct {
Email string
Password string
Valid bool
}

func (a *APIConfig) HandlePostSignup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
signupDetails := SignupDetails{
signupDetails := SignupForm{
Email: r.FormValue("email"),
Password: r.FormValue("password"),
}

signupPageData := SignupPageData{
Title: "TailScribe - Sign Up",
SignupDetails: signupDetails,
Title: "TailScribe - Sign Up",
SignupForm: signupDetails,
}

tmpl := template.Must(template.ParseFiles(
Expand Down Expand Up @@ -154,76 +165,139 @@ func (a *APIConfig) HandlePostSignup(w http.ResponseWriter, r *http.Request) {
return
}

tokenString, err := auth.MakeJWT(user.ID, a.Env.Secret)
err = a.createAndAttachSessionCookies(&w, user)
if err != nil {
log.Fatal(err)
signupDetails.Valid = false
w.WriteHeader(http.StatusInternalServerError)
w.WriteHeader(http.StatusBadRequest)
err = tmpl.Execute(w, signupPageData)
if err != nil {
log.Fatal(err)
}
return
}

refreshTokenString, err := auth.MakeRefreshToken()
http.Redirect(w, r, "/add_new_pet", http.StatusFound)
}

func expireCookie(w *http.ResponseWriter, cookie_name string) {
http.SetCookie(*w, &http.Cookie{
Name: cookie_name,
Value: "",
Expires: time.Unix(0, 0),
HttpOnly: true,
})
}

func (a *APIConfig) createAndAttachSessionCookies(
w *http.ResponseWriter,
user database.User,
) error {
tokenString, err := auth.MakeJWT(user.ID, a.Env.Secret)
if err != nil {
signupDetails.Valid = false
w.WriteHeader(http.StatusInternalServerError)
err = tmpl.Execute(w, signupPageData)
if err != nil {
log.Fatal(err)
}
return
return err
}

newPetPageData := BasePageData{
Title: "Add a new Pet",
refreshTokenString, err := auth.MakeRefreshToken()
if err != nil {
return err
}

// Create new template that points to new pet page.
tmpl = template.Must(template.ParseFiles(
"./templates/new_pet.html",
"./templates/base.html",
))

http.SetCookie(w, &http.Cookie{
http.SetCookie(*w, &http.Cookie{
Name: "token",
Value: tokenString,
Expires: time.Now().Add(time.Hour * 24),
HttpOnly: true,
Secure: true,
Domain: "/",
// Domain: "/",
SameSite: http.SameSiteStrictMode,
})
http.SetCookie(w, &http.Cookie{
http.SetCookie(*w, &http.Cookie{
Name: "refresh_token",
Value: refreshTokenString,
Expires: time.Now().Add(time.Hour * 30 * 24),
HttpOnly: true,
Domain: "/",
// Domain: "/",
SameSite: http.SameSiteStrictMode,
})
w.WriteHeader(http.StatusCreated)
err = tmpl.Execute(w, newPetPageData)

return nil
}

func RejectPostLogin(
w http.ResponseWriter,
tmpl *template.Template,
loginDetails *LoginForm,
loginPageData *LoginPageData,
status int) error {

loginDetails.Valid = false
w.WriteHeader(status)

err := tmpl.Execute(w, loginPageData)

return err
}

func (a *APIConfig) HandlePostLogin(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
loginDetails := LoginForm{
Email: r.FormValue("email"),
Password: r.FormValue("password"),
}

loginPageData := LoginPageData{
Title: "TailScribe - Log In",
LoginForm: loginDetails,
}

tmpl := template.Must(template.ParseFiles(
"./templates/login.html",
"./templates/base.html",
))

email := sql.NullString{
String: loginDetails.Email,
Valid: true,
}

user, err := a.Db.GetUserByEmail(ctx, email)
if err != nil {
err = RejectPostLogin(w, tmpl, &loginDetails, &loginPageData, http.StatusUnauthorized)
if err != nil {
log.Fatal(err)
}
return
}

valid := auth.CheckPasswordHash(loginDetails.Password, user.Password.String)

if !valid {
err = RejectPostLogin(w, tmpl, &loginDetails, &loginPageData, http.StatusUnauthorized)
if err != nil {
log.Fatal(err)
}
return
}

err = a.createAndAttachSessionCookies(&w, user)
if err != nil {
log.Fatal(err)
err = RejectPostLogin(w, tmpl, &loginDetails, &loginPageData, http.StatusInternalServerError)
if err != nil {
log.Fatal(err)
}
return
}
}

func expireCookie(w *http.ResponseWriter, cookie_name string) {
http.SetCookie(*w, &http.Cookie{
Name: cookie_name,
Value: "",
Expires: time.Unix(0, 0),
HttpOnly: true,
})
http.Redirect(w, r, "/dashboard", http.StatusFound)
}

func (a *APIConfig) HandlePostLogout(w http.ResponseWriter, r *http.Request) {
expireCookie(&w, "token")
expireCookie(&w, "refresh_token")

http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/", http.StatusFound)
}

func (a *APIConfig) HandleAttributions(w http.ResponseWriter, r *http.Request) {
Expand Down
70 changes: 67 additions & 3 deletions internal/api/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ func TestHandlePostSignup(t *testing.T) {
apiCfg.HandlePostSignup(response, request)

result := response.Result()
assert.Equal(t, 201, result.StatusCode)
assert.Equal(t, 302, result.StatusCode)

assert.Equal(t, result.Header.Get("Location"), "/add_new_pet")

cookies := result.Cookies()
assert.NotNil(t, cookies[0])
assert.Equal(t, "token", cookies[0].Name)
Expand Down Expand Up @@ -137,7 +140,7 @@ func TestHandleLogout(t *testing.T) {

logoutResult := logoutResponse.Result()

assert.Equal(t, 307, logoutResult.StatusCode)
assert.Equal(t, 302, logoutResult.StatusCode)
logoutCookies := logoutResult.Cookies()
assert.NotNil(t, logoutCookies[0])
assert.Equal(t, "token", logoutCookies[0].Name)
Expand All @@ -157,7 +160,7 @@ func TestHandleLogout(t *testing.T) {

logoutResult := logoutResponse.Result()

assert.Equal(t, 307, logoutResult.StatusCode)
assert.Equal(t, 302, logoutResult.StatusCode)
logoutCookies := logoutResult.Cookies()
assert.NotNil(t, logoutCookies[0])
assert.Equal(t, "token", logoutCookies[0].Name)
Expand All @@ -168,6 +171,67 @@ func TestHandleLogout(t *testing.T) {
})
}

func TestHandlePostLogin(t *testing.T) {
t.Run("Successfully logs a user in", func(t *testing.T) {
formData := url.Values{
"email": {"testEmail2@email.com"},
"password": {"password123"},
}

request, _ := http.NewRequest(http.MethodPost, "/signup", strings.NewReader(formData.Encode()))
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
response := httptest.NewRecorder()
apiCfg := NewAPIConfig(TestEnvVars, DbQueries)
apiCfg.HandlePostSignup(response, request)

result := response.Result()
assert.Equal(t, 302, result.StatusCode)

loginFormData := url.Values{
"email": {"testEmail2@email.com"},
"password": {"password123"},
}

loginRequest, _ := http.NewRequest(http.MethodPost, "/login", strings.NewReader(loginFormData.Encode()))
loginRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")

loginResponse := httptest.NewRecorder()
loginApiCfg := NewAPIConfig(TestEnvVars, DbQueries)
loginApiCfg.HandlePostLogin(loginResponse, loginRequest)

loginResult := loginResponse.Result()
assert.Equal(t, 302, loginResult.StatusCode)

assert.Equal(t, loginResult.Header.Get("Location"), "/dashboard")

cookies := loginResult.Cookies()
assert.NotNil(t, cookies[0])
assert.Equal(t, "token", cookies[0].Name)
assert.NotNil(t, cookies[1])
assert.Equal(t, "refresh_token", cookies[1].Name)
})

t.Run("Fails to log a user in if invalid username/password", func(t *testing.T) {
loginFormData := url.Values{
"email": {"testEmail3@email.com"},
"password": {"password123"},
}

loginRequest, _ := http.NewRequest(http.MethodPost, "/login", strings.NewReader(loginFormData.Encode()))
loginRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")

loginResponse := httptest.NewRecorder()
loginApiCfg := NewAPIConfig(TestEnvVars, DbQueries)
loginApiCfg.HandlePostLogin(loginResponse, loginRequest)

loginResult := loginResponse.Result()
assert.Equal(t, 401, loginResult.StatusCode)

cookies := loginResult.Cookies()
assert.Len(t, cookies, 0)
})
}

func TestGetAttributions(t *testing.T) {
request, _ := http.NewRequest(http.MethodGet, "/attributions", nil)
response := httptest.NewRecorder()
Expand Down
29 changes: 29 additions & 0 deletions internal/database/users.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func main() {
mux.HandleFunc("/", apiCfg.HandleIndex)
mux.HandleFunc("GET /signup", apiCfg.HandleSignupPage)
mux.HandleFunc("POST /signup", apiCfg.HandlePostSignup)
mux.HandleFunc("POST /login", apiCfg.HandlePostLogin)
mux.HandleFunc("POST /logout", apiCfg.HandlePostLogout)
mux.HandleFunc("/attributions", apiCfg.HandleAttributions)
mux.HandleFunc("/terms", apiCfg.HandleTerms)
Expand Down
7 changes: 6 additions & 1 deletion sql/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ VALUES (
RETURNING *;

-- name: DeleteUsers :exec
DELETE FROM users;
DELETE FROM users;

-- name: GetUserByEmail :one
SELECT *
FROM users
WHERE email = $1;
Loading