welcome: username error handling (#75)

* username error handling welcome ui

* added short username check with separate error handling function

* nothing happaned here

* added username error handling, form saves info

* pull75 fixes

* pull75 fixes ui

* CSS change

* separate css for errors, added logic for several type of errors

* rename extractErrorMessage

* validation changes

* added validations in api

* changed rendering template, recives errors in JSON format

* rolled back schema and makefile in kratos

* changes in HTML

* combined kratos and manual validations

* fixed rendering and handling JSON error response

* rollback unused index.html

* minor fixes

* refactored the repeated logic of Errors into a separate function

* rollback

* refactor: group errors and form data together

* rollback picocss version

* use picocss 2.0.6

---------

Co-authored-by: Giorgi Lekveishvili <lekva@gl-mbp-m1-max.local>
diff --git a/core/auth/ui/api.go b/core/auth/ui/api.go
index a0065a6..ab0ea76 100644
--- a/core/auth/ui/api.go
+++ b/core/auth/ui/api.go
@@ -4,7 +4,6 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io"
 	"net/http"
 
 	"github.com/gorilla/mux"
@@ -16,6 +15,14 @@
 	kratosAddr string
 }
 
+type ErrorResponse struct {
+	Error struct {
+		Code    int    `json:"code"`
+		Status  string `json:"status"`
+		Message string `json:"message"`
+	} `json:"error"`
+}
+
 func NewAPIServer(port int, kratosAddr string) *APIServer {
 	r := mux.NewRouter()
 	serv := &http.Server{
@@ -52,25 +59,85 @@
 	Password string `json:"password,omitempty"`
 }
 
+func extractKratosErrorMessage(errResp ErrorResponse) []ValidationError {
+	var errors []ValidationError
+	switch errResp.Error.Status {
+	case "Conflict":
+		errors = append(errors, ValidationError{"username", "Username is not available."})
+	case "Bad Request":
+		errors = append(errors, ValidationError{"username", "Username is less than 3 characters."})
+	default:
+		errors = append(errors, ValidationError{"username", "Unexpexted Error."})
+	}
+	return errors
+}
+
+type ValidationError struct {
+	Field   string `json:"field"`
+	Message string `json:"message"`
+}
+
+type CombinedErrors struct {
+	Errors []ValidationError `json:"errors"`
+}
+
+func validateUsername(username string) []ValidationError {
+	var errors []ValidationError
+	if len(username) < 3 {
+		errors = append(errors, ValidationError{"username", "Username must be at least 3 characters long."})
+	}
+	// TODO other validations
+	return errors
+}
+
+func validatePassword(password string) []ValidationError {
+	var errors []ValidationError
+	if len(password) < 6 {
+		errors = append(errors, ValidationError{"password", "Password must be at least 6 characters long."})
+	}
+	// TODO other validations
+	return errors
+}
+
+func replyWithErrors(w http.ResponseWriter, errors []ValidationError) {
+	response := CombinedErrors{Errors: errors}
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusBadRequest)
+	if err := json.NewEncoder(w).Encode(response); err != nil {
+		http.Error(w, "failed to decode", http.StatusInternalServerError)
+		return
+	}
+}
+
 func (s *APIServer) identityCreate(w http.ResponseWriter, r *http.Request) {
 	var req identityCreateReq
 	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 		http.Error(w, "request can not be parsed", http.StatusBadRequest)
 		return
 	}
+	usernameErrors := validateUsername(req.Username)
+	passwordErrors := validatePassword(req.Password)
+	allErrors := append(usernameErrors, passwordErrors...)
+	if len(allErrors) > 0 {
+		replyWithErrors(w, allErrors)
+		return
+	}
 	var buf bytes.Buffer
 	fmt.Fprintf(&buf, identityCreateTmpl, req.Password, req.Username)
 	resp, err := http.Post(s.identitiesEndpoint(), "application/json", &buf)
 	if err != nil {
 		http.Error(w, "failed", http.StatusInternalServerError)
 		return
-	} else if resp.StatusCode != http.StatusCreated {
-		var buf bytes.Buffer
-		if _, err := io.Copy(&buf, resp.Body); err != nil {
-			http.Error(w, "failed to copy response body", http.StatusInternalServerError)
-		} else {
-			http.Error(w, buf.String(), resp.StatusCode)
+	}
+	if resp.StatusCode != http.StatusCreated {
+		var e ErrorResponse
+		if err := json.NewDecoder(resp.Body).Decode(&e); err != nil {
+			http.Error(w, "failed to decode", http.StatusInternalServerError)
+			return
 		}
+		errorMessages := extractKratosErrorMessage(e)
+		replyWithErrors(w, errorMessages)
+		return
 	}
 }