Auth: Add page to change password.

Configure launcher as a default return to address.

Use standard X-Forwarded-User instead of custom X-User header.
Add X-Forwarded-UserId header holding user unique identificator.

Change-Id: Ib2e6329ba9fb91d2cc9a86b0c5fc78898769e3b8
diff --git a/core/auth/ui/api.go b/core/auth/ui/api.go
index 6fb7426..b8c5bad 100644
--- a/core/auth/ui/api.go
+++ b/core/auth/ui/api.go
@@ -4,7 +4,9 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"io"
 	"net/http"
+	"net/url"
 	"strings"
 	"unicode"
 
@@ -111,7 +113,7 @@
 		}
 	}
 	if !digit || !lowerCase || !upperCase || !special {
-		errors = append(errors, ValidationError{"password", "Password must contain at least one ditig, lower/upper and special character"})
+		errors = append(errors, ValidationError{"password", "Password must contain at least one digit, lower&upper case and special characters"})
 	}
 	// TODO other validations
 	return errors
@@ -167,6 +169,74 @@
 	}
 }
 
+type changePasswordReq struct {
+	Id       string `json:"id,omitempty"`
+	Username string `json:"username,omitempty"`
+	Password string `json:"password,omitempty"`
+}
+
+func (s *APIServer) apiPasswordChange(id, username, password string) ([]ValidationError, error) {
+	var usernameErrors []ValidationError
+	passwordErrors := validatePassword(password)
+	allErrors := append(usernameErrors, passwordErrors...)
+	if len(allErrors) > 0 {
+		return allErrors, nil
+	}
+	var kreq kratosIdentityCreateReq
+	kreq.Credentials.Password.Config.Password = password
+	kreq.SchemaID = "user"
+	kreq.State = "active"
+	kreq.Traits.Username = username
+	var buf bytes.Buffer
+	if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
+		return nil, err
+	}
+	c := http.Client{}
+	addr, err := url.Parse(s.identityEndpoint(id))
+	if err != nil {
+		return nil, err
+	}
+	hreq := &http.Request{
+		Method: http.MethodPut,
+		URL:    addr,
+		Header: http.Header{"Content-Type": []string{"application/json"}},
+		Body:   io.NopCloser(&buf),
+	}
+	resp, err := c.Do(hreq)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		var buf bytes.Buffer
+		io.Copy(&buf, resp.Body)
+		respS := buf.String()
+		fmt.Printf("PASSWORD CHANGE ERROR: %s\n", respS)
+		var e ErrorResponse
+		if err := json.NewDecoder(bytes.NewReader([]byte(respS))).Decode(&e); err != nil {
+			return nil, err
+		}
+		return extractKratosErrorMessage(e), nil
+	}
+	return nil, nil
+}
+
+func (s *APIServer) passwordChange(w http.ResponseWriter, r *http.Request) {
+	var req changePasswordReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	if verr, err := s.apiPasswordChange(req.Id, req.Username, req.Password); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	} else if len(verr) > 0 {
+		replyWithErrors(w, verr)
+	}
+}
+
 func (s *APIServer) identitiesEndpoint() string {
 	return fmt.Sprintf("%s/admin/identities", s.kratosAddr)
 }
+
+func (s *APIServer) identityEndpoint(id string) string {
+	return fmt.Sprintf("%s/admin/identities/%s", s.kratosAddr, id)
+}
diff --git a/core/auth/ui/main.go b/core/auth/ui/main.go
index 3f77bc8..73561e7 100644
--- a/core/auth/ui/main.go
+++ b/core/auth/ui/main.go
@@ -24,11 +24,10 @@
 var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
 var hydra = flag.String("hydra", "hydra.pcloud", "Hydra admin server address")
 var emailDomain = flag.String("email-domain", "lekva.me", "Email domain")
-
 var apiPort = flag.Int("api-port", 8081, "API Port to listen on")
 var kratosAPI = flag.String("kratos-api", "", "Kratos API address")
-
 var enableRegistration = flag.Bool("enable-registration", false, "If true account registration will be enabled")
+var defaultReturnTo = flag.String("default-return-to", "", "Default redirect address after login")
 
 var ErrNotLoggedIn = errors.New("Not logged in")
 
@@ -39,10 +38,12 @@
 var static embed.FS
 
 type Templates struct {
-	WhoAmI   *template.Template
-	Register *template.Template
-	Login    *template.Template
-	Consent  *template.Template
+	WhoAmI                *template.Template
+	Register              *template.Template
+	Login                 *template.Template
+	Consent               *template.Template
+	ChangePassword        *template.Template
+	ChangePasswordSuccess *template.Template
 }
 
 func ParseTemplates(fs embed.FS) (*Templates, error) {
@@ -73,7 +74,15 @@
 	if err != nil {
 		return nil, err
 	}
-	return &Templates{whoami, register, login, consent}, nil
+	changePassword, err := parse("templates/change-password.html")
+	if err != nil {
+		return nil, err
+	}
+	changePasswordSuccess, err := parse("templates/change-password-success.html")
+	if err != nil {
+		return nil, err
+	}
+	return &Templates{whoami, register, login, consent, changePassword, changePasswordSuccess}, nil
 }
 
 type Server struct {
@@ -83,15 +92,25 @@
 	hydra              *HydraClient
 	tmpls              *Templates
 	enableRegistration bool
+	api                *APIServer
+	defaultReturnTo    string
 }
 
-func NewServer(port int, kratos string, hydra *HydraClient, tmpls *Templates, enableRegistration bool) *Server {
+func NewServer(
+	port int,
+	kratos string,
+	hydra *HydraClient,
+	tmpls *Templates,
+	enableRegistration bool,
+	api *APIServer,
+	defaultReturnTo string,
+) *Server {
 	r := mux.NewRouter()
 	serv := &http.Server{
 		Addr:    fmt.Sprintf(":%d", port),
 		Handler: r,
 	}
-	return &Server{r, serv, kratos, hydra, tmpls, enableRegistration}
+	return &Server{r, serv, kratos, hydra, tmpls, enableRegistration, api, defaultReturnTo}
 }
 
 func cacheControlWrapper(h http.Handler) http.Handler {
@@ -115,6 +134,8 @@
 	s.r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
 	s.r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
 	s.r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
+	s.r.Path("/change-password").Methods("POST").HandlerFunc(s.changePassword)
+	s.r.Path("/change-password").Methods("GET").HandlerFunc(s.changePasswordForm)
 	s.r.Path("/").HandlerFunc(s.whoami)
 	return s.serv.ListenAndServe()
 }
@@ -225,7 +246,7 @@
 		return
 	}
 	if challenge, ok := r.Form["login_challenge"]; ok {
-		username, err := getWhoAmIFromKratos(r.Cookies())
+		_, username, err := getWhoAmIFromKratos(r.Cookies())
 		if err != nil && err != ErrNotLoggedIn {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -246,7 +267,10 @@
 			HttpOnly: true,
 		})
 	}
-	returnTo := r.Form.Get("return_to")
+	returnTo := r.FormValue("return_to")
+	if returnTo == "" && s.defaultReturnTo != "" {
+		returnTo = s.defaultReturnTo
+	}
 	flow, ok := r.Form["flow"]
 	if !ok {
 		addr := s.kratos + "/self-service/login/browser"
@@ -358,10 +382,10 @@
 	return lr.LogoutURL, nil
 }
 
-func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
+func getWhoAmIFromKratos(cookies []*http.Cookie) (string, string, error) {
 	jar, err := cookiejar.New(nil)
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	client := &http.Client{
 		Jar: jar,
@@ -371,25 +395,32 @@
 	}
 	b, err := url.Parse(*kratos + "/sessions/whoami")
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	client.Jar.SetCookies(b, cookies)
 	resp, err := client.Get(*kratos + "/sessions/whoami")
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	respBody, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	username, err := regogo.Get(string(respBody), "input.identity.traits.username")
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	if username.String() == "" {
-		return "", ErrNotLoggedIn
+		return "", "", ErrNotLoggedIn
 	}
-	return username.String(), nil
+	id, err := regogo.Get(string(respBody), "input.identity.id")
+	if err != nil {
+		return "", "", err
+	}
+	if id.String() == "" {
+		return "", "", ErrNotLoggedIn
+	}
+	return id.String(), username.String(), nil
 
 }
 
@@ -430,7 +461,6 @@
 		"identifier": []string{r.FormValue("username")},
 	}
 	resp, err := postFormToKratos("login", flow[0], r.Cookies(), req)
-	fmt.Printf("--- %d\n", resp.StatusCode)
 	var vv bytes.Buffer
 	io.Copy(&vv, resp.Body)
 	fmt.Println(vv.String())
@@ -451,7 +481,7 @@
 		http.SetCookie(w, c)
 	}
 	if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
-		username, err := getWhoAmIFromKratos(resp.Cookies())
+		_, username, err := getWhoAmIFromKratos(resp.Cookies())
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
@@ -481,7 +511,7 @@
 }
 
 func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
-	if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
+	if _, username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
 		if errors.Is(err, ErrNotLoggedIn) {
 			http.Redirect(w, r, "/login", http.StatusSeeOther)
 			return
@@ -510,7 +540,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	username, err := getWhoAmIFromKratos(r.Cookies())
+	_, username, err := getWhoAmIFromKratos(r.Cookies())
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -538,7 +568,7 @@
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
-	username, err := getWhoAmIFromKratos(r.Cookies())
+	_, username, err := getWhoAmIFromKratos(r.Cookies())
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -560,15 +590,67 @@
 	}
 }
 
+type changePasswordData struct {
+	Username       string
+	Password       string
+	PasswordErrors []ValidationError
+}
+
+func (s *Server) changePasswordForm(w http.ResponseWriter, r *http.Request) {
+	_, username, err := getWhoAmIFromKratos(r.Cookies())
+	if err != nil {
+		if errors.Is(err, ErrNotLoggedIn) {
+			http.Redirect(w, r, "/", http.StatusSeeOther)
+		} else {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+		return
+	}
+	if err := s.tmpls.ChangePassword.Execute(w, changePasswordData{Username: username}); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+	password := r.FormValue("password")
+	id, username, err := getWhoAmIFromKratos(r.Cookies())
+	if err != nil {
+		if errors.Is(err, ErrNotLoggedIn) {
+			http.Redirect(w, r, "/", http.StatusSeeOther)
+		} else {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+		return
+	}
+	if verr, err := s.api.apiPasswordChange(id, username, password); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	} else if len(verr) > 0 {
+		if err := s.tmpls.ChangePassword.Execute(w, changePasswordData{username, password, verr}); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	} else {
+		if err := s.tmpls.ChangePasswordSuccess.Execute(w, nil); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+	}
+}
+
 func main() {
 	flag.Parse()
 	t, err := ParseTemplates(tmpls)
 	if err != nil {
 		log.Fatal(err)
 	}
+	api := NewAPIServer(*apiPort, *kratosAPI)
 	go func() {
-		s := NewAPIServer(*apiPort, *kratosAPI)
-		log.Fatal(s.Start())
+		log.Fatal(api.Start())
 	}()
 	func() {
 		s := NewServer(
@@ -577,6 +659,8 @@
 			NewHydraClient(*hydra),
 			t,
 			*enableRegistration,
+			api,
+			*defaultReturnTo,
 		)
 		log.Fatal(s.Start())
 	}()
diff --git a/core/auth/ui/static/main.css b/core/auth/ui/static/main.css
index 454eb2d..a261094 100644
--- a/core/auth/ui/static/main.css
+++ b/core/auth/ui/static/main.css
@@ -83,3 +83,7 @@
 label {
   color: white;
 }
+
+.error-message {
+  color: var(--pico-primary-hover);
+}
diff --git a/core/auth/ui/templates/base.html b/core/auth/ui/templates/base.html
index 26817d3..624f908 100644
--- a/core/auth/ui/templates/base.html
+++ b/core/auth/ui/templates/base.html
@@ -3,7 +3,7 @@
 	<head>
         <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
 		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
-		<link rel="stylesheet" href="/static/main.css?v=0.0.1">
+		<link rel="stylesheet" href="/static/main.css?v=0.0.2">
 		<meta charset="utf-8" />
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
 		<title>{{ block "title" . }}Title{{ end }}</title>
diff --git a/core/auth/ui/templates/change-password-success.html b/core/auth/ui/templates/change-password-success.html
new file mode 100644
index 0000000..61c8e74
--- /dev/null
+++ b/core/auth/ui/templates/change-password-success.html
@@ -0,0 +1,6 @@
+{{ define "title" }}dodo: password changed{{ end }}
+{{ define "main" }}
+<div>
+	<p>Password changed successfully.</p>
+</div>
+{{ end }}
diff --git a/core/auth/ui/templates/change-password.html b/core/auth/ui/templates/change-password.html
new file mode 100644
index 0000000..4d0b487
--- /dev/null
+++ b/core/auth/ui/templates/change-password.html
@@ -0,0 +1,23 @@
+{{ define "title" }}dodo: change password{{ end }}
+{{ define "main" }}
+<div class="form-container">
+	<div class="logo">
+		<span>do</span><span>do:</span>
+	</div>
+	<form action="" method="POST">
+		<label>
+			new password
+			<input type="password" name="password" aria-label="Password" value="{{ .Password }}" aria-invalid="{{ if .PasswordErrors }}true{{ else }}undefined{{ end }}" required/>
+		</label>
+		{{ if .PasswordErrors }}
+		{{ range .PasswordErrors }}
+		<small class="error-message" aria-live="assertive">
+			{{ .Message }}
+		</small>
+		{{ end }}
+		{{ end }}
+		<button type="submit">change password</button>
+		<input type="hidden" name="username" value="{{ .Username }}" />
+	</form>
+</div>
+{{ end }}
diff --git a/core/auth/ui/templates/consent.html b/core/auth/ui/templates/consent.html
index 3a504a5..703434b 100644
--- a/core/auth/ui/templates/consent.html
+++ b/core/auth/ui/templates/consent.html
@@ -1,4 +1,4 @@
-{{ define "title" }}Consent{{ end }}
+{{ define "title" }}dodo: consent{{ end }}
 {{ define "main" }}
 <form action="" method="POST">
 	{{ range . }}
diff --git a/core/auth/ui/templates/login.html b/core/auth/ui/templates/login.html
index a80ecc7..0d961b5 100644
--- a/core/auth/ui/templates/login.html
+++ b/core/auth/ui/templates/login.html
@@ -1,4 +1,4 @@
-{{ define "title" }}Sign in{{ end }}
+{{ define "title" }}dodo: sign in{{ end }}
 {{ define "main" }}
 <div>
 	<div class="logo">
diff --git a/core/auth/ui/templates/register.html b/core/auth/ui/templates/register.html
index 22d69a5..626dc52 100644
--- a/core/auth/ui/templates/register.html
+++ b/core/auth/ui/templates/register.html
@@ -1,4 +1,4 @@
-{{ define "title" }}Create Account{{ end }}
+{{ define "title" }}dodo: create account{{ end }}
 {{ define "main" }}
 <form action="" method="POST">
 	<input type="text" name="username" placeholder="Username" autofocus required />
diff --git a/core/auth/ui/templates/whoami.html b/core/auth/ui/templates/whoami.html
index 2001c9f..944ed28 100644
--- a/core/auth/ui/templates/whoami.html
+++ b/core/auth/ui/templates/whoami.html
@@ -1,4 +1,4 @@
-{{ define "title" }}Who Am I{{ end }}
+{{ define "title" }}dodo: who am i{{ end }}
 {{ define "main" }}
 Hello {{.}}!
 <a href="/logout" role="button">logout</a>