Skip to content

Commit 42f400b

Browse files
committed
Complete all challenges
1 parent a4a0232 commit 42f400b

13 files changed

+292
-3
lines changed

cmd/web/handlers.go

+96-2
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,9 @@ func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) {
162162
}
163163

164164
type userLoginForm struct {
165-
Email string `form:"email"`
166-
Password string `form:"password"`
165+
Email string `form:"email"`
166+
Password string `form:"password"`
167+
167168
validator.Validator `form:"-"`
168169
}
169170

@@ -217,6 +218,12 @@ func (app *application) userLoginPost(w http.ResponseWriter, r *http.Request) {
217218

218219
app.sessionManager.Put(r.Context(), "authenticatedUserID", id)
219220

221+
path := app.sessionManager.PopString(r.Context(), "redirectPathAfterLogin")
222+
if path != "" {
223+
http.Redirect(w, r, path, http.StatusSeeOther)
224+
return
225+
}
226+
220227
http.Redirect(w, r, "/snippet/create", http.StatusSeeOther)
221228
}
222229

@@ -233,3 +240,90 @@ func (app *application) userLogoutPost(w http.ResponseWriter, r *http.Request) {
233240

234241
http.Redirect(w, r, "/", http.StatusSeeOther)
235242
}
243+
244+
func (app *application) about(w http.ResponseWriter, r *http.Request) {
245+
data := app.newTemplateData(r)
246+
247+
app.render(w, http.StatusOK, "about.html", data)
248+
}
249+
250+
func (app *application) accountView(w http.ResponseWriter, r *http.Request) {
251+
userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")
252+
253+
user, err := app.users.Get(userID)
254+
if err != nil {
255+
if errors.Is(err, models.ErrNoRecord) {
256+
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
257+
} else {
258+
app.serverError(w, err)
259+
}
260+
261+
return
262+
}
263+
264+
data := app.newTemplateData(r)
265+
data.User = user
266+
267+
app.render(w, http.StatusOK, "account.html", data)
268+
}
269+
270+
type accountPasswordUpdateForm struct {
271+
CurrentPassword string `form:"currentPassword"`
272+
NewPassword string `form:"newPassword"`
273+
NewPasswordConfirmation string `form:"newPasswordConfirmation"`
274+
275+
validator.Validator `form:"-"`
276+
}
277+
278+
func (app *application) accountPasswordUpdate(w http.ResponseWriter, r *http.Request) {
279+
data := app.newTemplateData(r)
280+
data.Form = accountPasswordUpdateForm{}
281+
282+
app.render(w, http.StatusOK, "password.html", data)
283+
}
284+
285+
func (app *application) accountPasswordUpdatePost(w http.ResponseWriter, r *http.Request) {
286+
var form accountPasswordUpdateForm
287+
288+
err := app.decodePostForm(r, &form)
289+
if err != nil {
290+
app.clientError(w, http.StatusBadRequest)
291+
return
292+
}
293+
294+
form.CheckField(validator.NotBlank(form.CurrentPassword), "currentPassword", "This field cannot be blank")
295+
form.CheckField(validator.NotBlank(form.NewPassword), "newPassword", "This field cannot be blank")
296+
form.CheckField(validator.MinChars(form.NewPassword, 8), "newPassword", "This field must be at least 8 characters long")
297+
form.CheckField(validator.NotBlank(form.NewPasswordConfirmation), "newPasswordConfirmation", "This field cannot be blank")
298+
form.CheckField(form.NewPassword == form.NewPasswordConfirmation, "newPasswordConfirmation", "Passwords do not match")
299+
300+
if !form.Valid() {
301+
data := app.newTemplateData(r)
302+
data.Form = form
303+
304+
app.render(w, http.StatusUnprocessableEntity, "password.html", data)
305+
return
306+
}
307+
308+
userID := app.sessionManager.GetInt(r.Context(), "authenticatedUserID")
309+
310+
err = app.users.PasswordUpdate(userID, form.CurrentPassword, form.NewPassword)
311+
if err != nil {
312+
if errors.Is(err, models.ErrInvalidCredentials) {
313+
form.AddFieldError("currentPassword", "Current password is incorrect")
314+
315+
data := app.newTemplateData(r)
316+
data.Form = form
317+
318+
app.render(w, http.StatusUnprocessableEntity, "password.html", data)
319+
} else if err != nil {
320+
app.serverError(w, err)
321+
}
322+
323+
return
324+
}
325+
326+
app.sessionManager.Put(r.Context(), "flash", "Your password has been updated successfully!")
327+
328+
http.Redirect(w, r, "/account/view", http.StatusSeeOther)
329+
}

cmd/web/handlers_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,32 @@ func TestUserSignup(t *testing.T) {
245245
})
246246
}
247247
}
248+
249+
func TestSnippetCreate(t *testing.T) {
250+
app := newTestApplication(t)
251+
ts := newTestServer(t, app.routes())
252+
defer ts.Close()
253+
254+
t.Run("Unauthenticated", func(t *testing.T) {
255+
code, headers, _ := ts.get(t, "/snippet/create")
256+
257+
assert.Equal(t, code, http.StatusSeeOther)
258+
assert.Equal(t, headers.Get("Location"), "/user/login")
259+
})
260+
261+
t.Run("Authenticated", func(t *testing.T) {
262+
_, _, body := ts.get(t, "/user/login")
263+
validCSRFToken := extractCSRFToken(t, body)
264+
265+
form := url.Values{}
266+
form.Add("email", "[email protected]")
267+
form.Add("password", "pa$$word")
268+
form.Add("csrf_token", validCSRFToken)
269+
ts.postForm(t, "/user/login", form)
270+
271+
code, _, body := ts.get(t, "/snippet/create")
272+
273+
assert.Equal(t, code, http.StatusOK)
274+
assert.StringContains(t, body, "<form action='/snippet/create' method='POST'>")
275+
})
276+
}

cmd/web/helpers.go

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ func (app *application) serverError(w http.ResponseWriter, err error) {
1616
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
1717
app.errorLog.Output(2, trace)
1818

19+
if app.debug {
20+
http.Error(w, trace, http.StatusInternalServerError)
21+
return
22+
}
23+
1924
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
2025
}
2126

cmd/web/main.go

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
)
1919

2020
type application struct {
21+
debug bool
2122
errorLog *log.Logger
2223
infoLog *log.Logger
2324
snippets models.SnippetModelInterface
@@ -43,6 +44,7 @@ func openDB(dsn string) (*sql.DB, error) {
4344
func main() {
4445
addr := flag.String("addr", ":4000", "HTTP network address")
4546
dsn := flag.String("dsn", "web:pass@tcp(localhost:3306)/snippetbox?parseTime=true", "MySQL data source name")
47+
debug := flag.Bool("debug", false, "Enable debug mode")
4648
flag.Parse()
4749

4850
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
@@ -67,6 +69,7 @@ func main() {
6769
sessionManager.Cookie.Secure = true
6870

6971
app := &application{
72+
debug: *debug,
7073
errorLog: errorLog,
7174
infoLog: infoLog,
7275
snippets: &models.SnippetModel{DB: db},

cmd/web/middleware.go

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func (app *application) recoverPanic(next http.Handler) http.Handler {
5858
func (app *application) requireAuthentication(next http.Handler) http.Handler {
5959
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6060
if !app.isAuthenticated(r) {
61+
app.sessionManager.Put(r.Context(), "redirectPathAfterLogin", r.URL.Path)
6162
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
6263
return
6364
}

cmd/web/routes.go

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func (app *application) routes() http.Handler {
2323
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
2424

2525
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
26+
router.Handler(http.MethodGet, "/about", dynamic.ThenFunc(app.about))
2627
router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
2728
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
2829
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
@@ -33,6 +34,9 @@ func (app *application) routes() http.Handler {
3334

3435
router.Handler(http.MethodGet, "/snippet/create", protected.ThenFunc(app.snippetCreate))
3536
router.Handler(http.MethodPost, "/snippet/create", protected.ThenFunc(app.snippetCreatePost))
37+
router.Handler(http.MethodGet, "/account/password/update", protected.ThenFunc(app.accountPasswordUpdate))
38+
router.Handler(http.MethodPost, "/account/password/update", protected.ThenFunc(app.accountPasswordUpdatePost))
39+
router.Handler(http.MethodGet, "/account/view", protected.ThenFunc(app.accountView))
3640
router.Handler(http.MethodPost, "/user/logout", protected.ThenFunc(app.userLogoutPost))
3741

3842
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)

cmd/web/templates.go

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type templateData struct {
1717
Flash string
1818
IsAuthenticated bool
1919
CSRFToken string
20+
User *models.User
2021
}
2122

2223
func humanDate(t time.Time) string {

internal/models/mocks/users.go

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package mocks
22

3-
import "lets-go-snippetbox/internal/models"
3+
import (
4+
"lets-go-snippetbox/internal/models"
5+
"time"
6+
)
47

58
type UserModel struct{}
69

@@ -29,3 +32,30 @@ func (m *UserModel) Exists(id int) (bool, error) {
2932
return false, nil
3033
}
3134
}
35+
36+
func (m *UserModel) Get(id int) (*models.User, error) {
37+
if id == 1 {
38+
u := &models.User{
39+
ID: 1,
40+
Name: "Alice",
41+
42+
Created: time.Now(),
43+
}
44+
45+
return u, nil
46+
}
47+
48+
return nil, models.ErrNoRecord
49+
}
50+
51+
func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error {
52+
if id == 1 {
53+
if currentPassword != "pa$$word" {
54+
return models.ErrInvalidCredentials
55+
}
56+
57+
return nil
58+
}
59+
60+
return models.ErrNoRecord
61+
}

internal/models/users.go

+49
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type UserModelInterface interface {
1414
Insert(name, email, password string) error
1515
Authenticate(email, password string) (int, error)
1616
Exists(id int) (bool, error)
17+
Get(id int) (*User, error)
18+
PasswordUpdate(id int, currentPassword, newPassword string) error
1719
}
1820

1921
type User struct {
@@ -90,3 +92,50 @@ func (m *UserModel) Exists(id int) (bool, error) {
9092

9193
return exists, err
9294
}
95+
96+
func (m *UserModel) Get(id int) (*User, error) {
97+
var user User
98+
99+
stmt := `SELECT id, name, email, created FROM users WHERE id = ?`
100+
101+
err := m.DB.QueryRow(stmt, id).Scan(&user.ID, &user.Name, &user.Email, &user.Created)
102+
if err != nil {
103+
if errors.Is(err, sql.ErrNoRows) {
104+
return nil, ErrNoRecord
105+
} else {
106+
return nil, err
107+
}
108+
}
109+
110+
return &user, nil
111+
}
112+
113+
func (m *UserModel) PasswordUpdate(id int, currentPassword, newPassword string) error {
114+
var currentHashedPassword []byte
115+
116+
stmt := "SELECT hashed_password FROM users WHERE id = ?"
117+
118+
err := m.DB.QueryRow(stmt, id).Scan(&currentHashedPassword)
119+
if err != nil {
120+
return err
121+
}
122+
123+
err = bcrypt.CompareHashAndPassword(currentHashedPassword, []byte(currentPassword))
124+
if err != nil {
125+
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
126+
return ErrInvalidCredentials
127+
} else {
128+
return err
129+
}
130+
}
131+
132+
newHashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), 12)
133+
if err != nil {
134+
return err
135+
}
136+
137+
stmt = "UPDATE users SET hashed_password = ? WHERE id = ?"
138+
139+
_, err = m.DB.Exec(stmt, string(newHashedPassword), id)
140+
return err
141+
}

ui/html/pages/about.html

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{define "title"}}About{{end}}
2+
3+
{{define "main"}}
4+
<h2>About</h2>
5+
<p>
6+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi at mauris dignissim,
7+
consectetur tellus in, fringilla ante. Pellentesque habitant morbi tristique senectus
8+
et netus et malesuada fames ac turpis egestas. Sed dignissim hendrerit scelerisque.
9+
</p>
10+
<p>
11+
Praesent a dignissim arcu. Cras a metus sagittis, pellentesque odio sit amet,
12+
lacinia velit. In hac habitasse platea dictumst.
13+
</p>
14+
{{end}}

ui/html/pages/account.html

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{{define "title"}}Your Account{{end}}
2+
3+
{{define "main"}}
4+
<h2>Your Account</h2>
5+
{{with .User}}
6+
<table>
7+
<tr>
8+
<th>Name</th>
9+
<td>{{.Name}}</td>
10+
</tr>
11+
<tr>
12+
<th>Email</th>
13+
<td>{{.Email}}</td>
14+
</tr>
15+
<tr>
16+
<th>Joined</th>
17+
<td>{{humanDate .Created}}</td>
18+
</tr>
19+
<tr>
20+
<th>Password</th>
21+
<td><a href="/account/password/update">Change password</a></td>
22+
</tr>
23+
</table>
24+
{{end}}
25+
{{end}}

ui/html/pages/password.html

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{{define "title"}}Change Password{{end}}
2+
3+
{{define "main"}}
4+
<h2>Change Password</h2>
5+
<form action='/account/password/update' method='POST' novalidate>
6+
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
7+
<div>
8+
<label>Current password:</label>
9+
{{with .Form.FieldErrors.currentPassword}}
10+
<label class='error'>{{.}}</label>
11+
{{end}}
12+
<input type='password' name='currentPassword'>
13+
</div>
14+
<div>
15+
<label>New password:</label>
16+
{{with .Form.FieldErrors.newPassword}}
17+
<label class='error'>{{.}}</label>
18+
{{end}}
19+
<input type='password' name='newPassword'>
20+
</div>
21+
<div>
22+
<label>Confirm new password:</label>
23+
{{with .Form.FieldErrors.newPasswordConfirmation}}
24+
<label class='error'>{{.}}</label>
25+
{{end}}
26+
<input type='password' name='newPasswordConfirmation'>
27+
</div>
28+
<div>
29+
<input type='submit' value='Change password'>
30+
</div>
31+
</form>
32+
{{end}}

0 commit comments

Comments
 (0)