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/Dockerfile b/core/auth/ui/Dockerfile
index 980e40e..6e8d93c 100644
--- a/core/auth/ui/Dockerfile
+++ b/core/auth/ui/Dockerfile
@@ -1,6 +1,5 @@
-FROM alpine:latest
+FROM gcr.io/distroless/static:nonroot
ARG TARGETARCH
COPY server_${TARGETARCH} /usr/bin/server
-RUN chmod +x /usr/bin/server
diff --git a/core/auth/ui/Makefile b/core/auth/ui/Makefile
index 53f7525..f3b9b63 100644
--- a/core/auth/ui/Makefile
+++ b/core/auth/ui/Makefile
@@ -1,3 +1,6 @@
+repo_name ?= dtabidze
+podman ?= docker
+
clean:
rm -f server server_*
@@ -19,15 +22,15 @@
go build -o server_amd64 *.go
push_arm64: clean build_arm64
- podman build --platform linux/arm64 --tag=giolekva/auth-ui:arm64 .
- podman push giolekva/auth-ui:arm64
+ $(podman) build --platform linux/arm64 --tag=$(repo_name)/auth-ui:arm64 .
+ $(podman) push $(repo_name)/auth-ui:arm64
push_amd64: clean build_amd64
- podman build --platform linux/amd64 --tag=giolekva/auth-ui:amd64 .
- podman push giolekva/auth-ui:amd64
+ $(podman) build --platform linux/amd64 --tag=$(repo_name)/auth-ui:amd64 .
+ $(podman) push $(repo_name)/auth-ui:amd64
push: push_arm64 push_amd64
- podman manifest create giolekva/auth-ui:latest giolekva/auth-ui:arm64 giolekva/auth-ui:amd64
- podman manifest push giolekva/auth-ui:latest docker://docker.io/giolekva/auth-ui:latest
- podman manifest rm giolekva/auth-ui:latest
+ $(podman) manifest create $(repo_name)/auth-ui:latest $(repo_name)/auth-ui:arm64 $(repo_name)/auth-ui:amd64
+ $(podman) manifest push $(repo_name)/auth-ui:latest
+ $(podman) manifest rm $(repo_name)/auth-ui:latest
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
}
}