ch17
This commit is contained in:
@@ -4,11 +4,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.local.lab/Lbenedar/snippetbox/internal/models"
|
"gitea.local.lab/Lbenedar/snippetbox/internal/models"
|
||||||
"gitea.local.lab/Lbenedar/snippetbox/internal/validator"
|
"gitea.local.lab/Lbenedar/snippetbox/internal/validator"
|
||||||
|
"gitea.local.lab/Lbenedar/snippetbox/ui"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,6 +35,20 @@ type userLoginForm struct {
|
|||||||
validator.Validator `form:"-"`
|
validator.Validator `form:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type accountViewForm struct {
|
||||||
|
Name string `form:"name"`
|
||||||
|
Email string `form:"email"`
|
||||||
|
Joined string `form:"joined"`
|
||||||
|
validator.Validator `form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type passwordChangeForm struct {
|
||||||
|
CurrentPassword string `form:"curr_pass"`
|
||||||
|
NewPassword string `form:"new_pass"`
|
||||||
|
ConfirmPassword string `form:"conf_pass"`
|
||||||
|
validator.Validator `form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
|
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
|
||||||
ts, ok := app.templateCache[page]
|
ts, ok := app.templateCache[page]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -210,7 +227,11 @@ func (app *application) userLoginPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.sessionManager.Put(r.Context(), "authenticatedUserID", id)
|
app.sessionManager.Put(r.Context(), "authenticatedUserID", id)
|
||||||
|
redirectPath := app.sessionManager.PopString(r.Context(), "redirectParent")
|
||||||
|
if redirectPath != "" {
|
||||||
|
http.Redirect(w, r, redirectPath, http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
http.Redirect(w, r, "/snippet/create", http.StatusSeeOther)
|
http.Redirect(w, r, "/snippet/create", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,3 +262,73 @@ func (app *application) userLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
func ping(w http.ResponseWriter, r *http.Request) {
|
func ping(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("OK"))
|
w.Write([]byte("OK"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *application) about(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := app.newTemplateData(r)
|
||||||
|
text, err := fs.ReadFile(ui.Files, "static/text/about.txt")
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.AboutText = string(text)
|
||||||
|
app.render(w, http.StatusOK, "about.tmpl", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) accountView(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := app.newTemplateData(r)
|
||||||
|
id := app.sessionManager.Get(r.Context(), "authenticatedUserID").(int)
|
||||||
|
|
||||||
|
user, err := app.users.GetById(id)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.Form = accountViewForm{Name: user.Name, Email: user.Email, Joined: humanDate(user.Created)}
|
||||||
|
app.render(w, http.StatusOK, "account.tmpl", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) accountChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := app.newTemplateData(r)
|
||||||
|
data.Form = passwordChangeForm{}
|
||||||
|
app.render(w, http.StatusOK, "password.tmpl", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) accountChangePasswordPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
form := passwordChangeForm{}
|
||||||
|
err := app.decodePostForm(r, &form)
|
||||||
|
if err != nil {
|
||||||
|
app.clientError(w, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.CheckField(validator.NotBlank(form.CurrentPassword), "curr_pass", "This field cannot be blank")
|
||||||
|
form.CheckField(validator.NotBlank(form.NewPassword), "new_pass", "This field cannot be blank")
|
||||||
|
form.CheckField(validator.MinChars(form.NewPassword, 8), "new_pass", "Minimum password length is 8 symbols")
|
||||||
|
form.CheckField(validator.NotBlank(form.ConfirmPassword), "conf_pass", "This field cannot be blank")
|
||||||
|
form.CheckField(strings.Compare(form.NewPassword, form.ConfirmPassword) == 0, "conf_pass", "Confirm password does not match")
|
||||||
|
|
||||||
|
if !form.Valid() {
|
||||||
|
data := app.newTemplateData(r)
|
||||||
|
data.Form = form
|
||||||
|
app.render(w, http.StatusUnprocessableEntity, "password.tmpl", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := app.sessionManager.Get(r.Context(), "authenticatedUserID").(int)
|
||||||
|
err = app.users.ChangePassword(id, form.CurrentPassword, form.NewPassword)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, models.ErrInvalidCredentials) {
|
||||||
|
form.AddNonFieldError("Password is incorrect")
|
||||||
|
|
||||||
|
data := app.newTemplateData(r)
|
||||||
|
data.Form = form
|
||||||
|
app.render(w, http.StatusUnprocessableEntity, "password.tmpl", data)
|
||||||
|
} else {
|
||||||
|
app.serverError(w, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.sessionManager.Put(r.Context(), "flash", "Password successfully changed!")
|
||||||
|
http.Redirect(w, r, "/account/view", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|||||||
@@ -192,3 +192,37 @@ func TestUserSignup(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSnippetCreate(t *testing.T) {
|
||||||
|
app := newTestApplication(t)
|
||||||
|
ts := newTestServer(t, app.routes())
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
const (
|
||||||
|
validPassword = "validPa$$word"
|
||||||
|
validEmail = "bob@example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("Unauthenticated", func(t *testing.T) {
|
||||||
|
code, head, _ := ts.get(t, "/snippet/create")
|
||||||
|
assert.Equal(t, code, http.StatusSeeOther)
|
||||||
|
assert.Equal(t, head.Get("Location"), "/user/login")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Authenticated", func(t *testing.T) {
|
||||||
|
_, _, body := ts.get(t, "/user/login")
|
||||||
|
validCsrfToken := extractCSRFToken(t, body)
|
||||||
|
t.Logf("CsrfToken: %s", validCsrfToken)
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("email", validEmail)
|
||||||
|
form.Add("password", validPassword)
|
||||||
|
form.Add("csrf_token", validCsrfToken)
|
||||||
|
code, _, _ := ts.postForm(t, "/user/login", form)
|
||||||
|
t.Logf("Auth result: %d", code)
|
||||||
|
|
||||||
|
code, _, body = ts.get(t, "/snippet/create")
|
||||||
|
assert.Equal(t, code, http.StatusOK)
|
||||||
|
assert.StringContains(t, body, "<form action='/snippet/create' method='POST'>")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ func (app *application) serverError(w http.ResponseWriter, err error) {
|
|||||||
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
|
trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
|
||||||
app.errorLog.Output(2, trace)
|
app.errorLog.Output(2, trace)
|
||||||
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
var errorText string
|
||||||
|
if app.debug {
|
||||||
|
errorText = trace
|
||||||
|
} else {
|
||||||
|
errorText = http.StatusText(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
http.Error(w, errorText, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) clientError(w http.ResponseWriter, status int) {
|
func (app *application) clientError(w http.ResponseWriter, status int) {
|
||||||
@@ -24,13 +30,13 @@ func (app *application) notFound(w http.ResponseWriter) {
|
|||||||
app.clientError(w, http.StatusNotFound)
|
app.clientError(w, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) decodePostForm(r *http.Request, snippetForm any) error {
|
func (app *application) decodePostForm(r *http.Request, templateForm any) error {
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.formDecoder.Decode(&snippetForm, r.PostForm)
|
err = app.formDecoder.Decode(&templateForm, r.PostForm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var invalidDecoderError *form.InvalidDecoderError
|
var invalidDecoderError *form.InvalidDecoderError
|
||||||
if errors.As(err, &invalidDecoderError) {
|
if errors.As(err, &invalidDecoderError) {
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ type application struct {
|
|||||||
templateCache map[string]*template.Template
|
templateCache map[string]*template.Template
|
||||||
formDecoder *form.Decoder
|
formDecoder *form.Decoder
|
||||||
sessionManager *scs.SessionManager
|
sessionManager *scs.SessionManager
|
||||||
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
addr string
|
addr string
|
||||||
staticDir string
|
staticDir string
|
||||||
dsn string
|
dsn string
|
||||||
|
debug bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -39,6 +41,7 @@ func main() {
|
|||||||
flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
|
flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
|
||||||
flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")
|
flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")
|
||||||
flag.StringVar(&cfg.dsn, "dsn", "web:pass@/snippetbox?parseTime=true", "MySQL data source name")
|
flag.StringVar(&cfg.dsn, "dsn", "web:pass@/snippetbox?parseTime=true", "MySQL data source name")
|
||||||
|
flag.BoolVar(&cfg.debug, "debug", false, "Option to enable debug mode")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
|
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
|
||||||
@@ -68,6 +71,7 @@ func main() {
|
|||||||
templateCache: templateCache,
|
templateCache: templateCache,
|
||||||
formDecoder: formDecoder,
|
formDecoder: formDecoder,
|
||||||
sessionManager: sessionManager,
|
sessionManager: sessionManager,
|
||||||
|
debug: cfg.debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ func secureHeaders(next http.Handler) http.Handler {
|
|||||||
func noSurf(next http.Handler) http.Handler {
|
func noSurf(next http.Handler) http.Handler {
|
||||||
csrfHanlder := nosurf.New(next)
|
csrfHanlder := nosurf.New(next)
|
||||||
csrfHanlder.SetBaseCookie(http.Cookie{
|
csrfHanlder.SetBaseCookie(http.Cookie{
|
||||||
HttpOnly: true,
|
HttpOnly: false,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Secure: true,
|
Secure: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
return csrfHanlder
|
return csrfHanlder
|
||||||
@@ -56,6 +56,7 @@ func (app *application) recoverPanic(next http.Handler) http.Handler {
|
|||||||
func (app *application) requireAuthentication(next http.Handler) http.Handler {
|
func (app *application) requireAuthentication(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if !app.isAuthenticated(r) {
|
if !app.isAuthenticated(r) {
|
||||||
|
app.sessionManager.Put(r.Context(), "redirectParent", r.URL.Path)
|
||||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func (app *application) routes() http.Handler {
|
|||||||
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
|
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
|
||||||
|
|
||||||
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
|
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
|
||||||
|
router.Handler(http.MethodGet, "/about", dynamic.ThenFunc(app.about))
|
||||||
router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
|
router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
|
||||||
|
|
||||||
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
|
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
|
||||||
@@ -33,6 +34,9 @@ func (app *application) routes() http.Handler {
|
|||||||
|
|
||||||
protected := dynamic.Append(app.requireAuthentication)
|
protected := dynamic.Append(app.requireAuthentication)
|
||||||
|
|
||||||
|
router.Handler(http.MethodGet, "/account/view", protected.ThenFunc(app.accountView))
|
||||||
|
router.Handler(http.MethodGet, "/account/password", protected.ThenFunc(app.accountChangePassword))
|
||||||
|
router.Handler(http.MethodPost, "/account/password", protected.ThenFunc(app.accountChangePasswordPost))
|
||||||
router.Handler(http.MethodGet, "/snippet/create", protected.ThenFunc(app.snippetCreate))
|
router.Handler(http.MethodGet, "/snippet/create", protected.ThenFunc(app.snippetCreate))
|
||||||
router.Handler(http.MethodPost, "/snippet/create", protected.ThenFunc(app.snippetCreatePost))
|
router.Handler(http.MethodPost, "/snippet/create", protected.ThenFunc(app.snippetCreatePost))
|
||||||
router.Handler(http.MethodPost, "/user/logout", protected.ThenFunc(app.userLogoutPost))
|
router.Handler(http.MethodPost, "/user/logout", protected.ThenFunc(app.userLogoutPost))
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type templateData struct {
|
|||||||
Flash string
|
Flash string
|
||||||
IsAuthenticated bool
|
IsAuthenticated bool
|
||||||
CSRFToken string
|
CSRFToken string
|
||||||
|
AboutText string
|
||||||
}
|
}
|
||||||
|
|
||||||
func humanDate(t time.Time) string {
|
func humanDate(t time.Time) string {
|
||||||
|
|||||||
@@ -29,3 +29,11 @@ func (m *UserModel) Exists(id int) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *UserModel) GetById(id int) (*models.User, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UserModel) ChangePassword(id int, curr_pass, new_pass string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ type UserModelInterface interface {
|
|||||||
Insert(name, email, password string) error
|
Insert(name, email, password string) error
|
||||||
Authenticate(email, password string) (int, error)
|
Authenticate(email, password string) (int, error)
|
||||||
Exists(id int) (bool, error)
|
Exists(id int) (bool, error)
|
||||||
|
GetById(id int) (*User, error)
|
||||||
|
ChangePassword(id int, curr_pass, new_pass string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserModel struct {
|
type UserModel struct {
|
||||||
@@ -82,3 +84,46 @@ func (m *UserModel) Exists(id int) (bool, error) {
|
|||||||
err := m.DB.QueryRow(stmt, id).Scan(&exists)
|
err := m.DB.QueryRow(stmt, id).Scan(&exists)
|
||||||
return exists, err
|
return exists, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *UserModel) GetById(id int) (*User, error) {
|
||||||
|
acc := User{}
|
||||||
|
|
||||||
|
stmt := `SELECT name, email, created FROM users WHERE id = ?`
|
||||||
|
err := a.DB.QueryRow(stmt, id).Scan(&acc.Name, &acc.Email, &acc.Created)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &acc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UserModel) ChangePassword(id int, curr_pass, new_pass string) error {
|
||||||
|
hashedNewPassword, err := bcrypt.GenerateFromPassword([]byte(new_pass), 12)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashedDBPassword []byte
|
||||||
|
stmt := `SELECT hashed_password FROM users WHERE id = ?`
|
||||||
|
err = a.DB.QueryRow(stmt, id).Scan(&hashedDBPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = bcrypt.CompareHashAndPassword(hashedDBPassword, []byte(curr_pass))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||||
|
return ErrInvalidCredentials
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt = `UPDATE users SET hashed_password = ? WHERE id = ?`
|
||||||
|
_, err = a.DB.Exec(stmt, hashedNewPassword, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
8
ui/html/pages/about.tmpl
Normal file
8
ui/html/pages/about.tmpl
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{{define "title"}}About{{end}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<h2>About</h2>
|
||||||
|
<div class='about'>
|
||||||
|
{{.AboutText}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
28
ui/html/pages/account.tmpl
Normal file
28
ui/html/pages/account.tmpl
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{{define "title"}}Account{{end}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<h2>Account</h2>
|
||||||
|
{{if .Form}}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>{{.Form.Name}}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>{{.Form.Email}}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Joined</th>
|
||||||
|
<th>{{.Form.Joined}}</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<p>There's nothing to see here... yet!</p>
|
||||||
|
{{end}}
|
||||||
|
<form action='/account/password' method='GET'>
|
||||||
|
<div>
|
||||||
|
<input type='submit' value='Change password'>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
35
ui/html/pages/password.tmpl
Normal file
35
ui/html/pages/password.tmpl
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{{define "title"}}Change Password{{end}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
<h2>Change Password</h2>
|
||||||
|
<form action='/account/password' method='POST' novalidate>
|
||||||
|
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
|
||||||
|
{{range .Form.NonFieldErrors}}
|
||||||
|
<div class='error'>{{.}}</div>
|
||||||
|
{{end}}
|
||||||
|
<div>
|
||||||
|
<label>Current password:</label>
|
||||||
|
{{with .Form.FieldErrors.curr_pass}}
|
||||||
|
<label class='error'>{{.}}</label>
|
||||||
|
{{end}}
|
||||||
|
<input type='password' name='curr_pass' value=''>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>New password:</label>
|
||||||
|
{{with .Form.FieldErrors.new_pass}}
|
||||||
|
<label class='error'>{{.}}</label>
|
||||||
|
{{end}}
|
||||||
|
<input type='password' name='new_pass'>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Confirm new password:</label>
|
||||||
|
{{with .Form.FieldErrors.conf_pass}}
|
||||||
|
<label class='error'>{{.}}</label>
|
||||||
|
{{end}}
|
||||||
|
<input type='password' name='conf_pass'>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type='submit' value='Change password'>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<div>
|
<div>
|
||||||
<a href='/'>Home</a>
|
<a href='/'>Home</a>
|
||||||
|
<a href='/about'>About</a>
|
||||||
{{if .IsAuthenticated}}
|
{{if .IsAuthenticated}}
|
||||||
<a href='/snippet/create'>Create snippet</a>
|
<a href='/snippet/create'>Create snippet</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{if .IsAuthenticated}}
|
{{if .IsAuthenticated}}
|
||||||
|
<a href='/account/view'>Account</a>
|
||||||
<form action='/user/logout' method='POST'>
|
<form action='/user/logout' method='POST'>
|
||||||
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
|
<input type='hidden' name='csrf_token' value='{{.CSRFToken}}'>
|
||||||
<button>Logout</button>
|
<button>Logout</button>
|
||||||
|
|||||||
1
ui/static/text/about.txt
Normal file
1
ui/static/text/about.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus magna quam, eleifend a metus at, tincidunt commodo enim. Integer ac mattis ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Maecenas non purus ullamcorper, tristique diam vel, sagittis neque. Fusce quam dolor, accumsan non leo sed, auctor rutrum felis. Duis semper tristique tellus, sed sagittis est lobortis nec. Curabitur porttitor lacus eget semper sodales. Duis diam nisl, maximus eu felis id, placerat pulvinar erat. Etiam elementum elit tellus, at auctor ante accumsan vitae. Nulla elit magna, molestie vitae nibh ut, vehicula accumsan ipsum. Ut id consequat ipsum, volutpat volutpat orci. Aenean rhoncus tellus lacus, nec tempor risus interdum sed. Proin quis quam in metus imperdiet lobortis. Duis euismod sem enim, volutpat mollis tortor posuere sed. Morbi condimentum eget dolor at accumsan.
|
||||||
Reference in New Issue
Block a user