blob: 6fb74260293dd025dfd44ae42703a4b6a0221bb8 [file] [log] [blame]
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +04001package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net/http"
giodcd9fef2024-09-26 14:42:59 +02008 "strings"
9 "unicode"
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040010
11 "github.com/gorilla/mux"
12)
13
14type APIServer struct {
15 r *mux.Router
16 serv *http.Server
17 kratosAddr string
18}
19
DTabidze52593392024-03-08 12:53:20 +040020type ErrorResponse struct {
21 Error struct {
22 Code int `json:"code"`
23 Status string `json:"status"`
24 Message string `json:"message"`
25 } `json:"error"`
26}
27
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040028func NewAPIServer(port int, kratosAddr string) *APIServer {
29 r := mux.NewRouter()
30 serv := &http.Server{
31 Addr: fmt.Sprintf(":%d", port),
32 Handler: r,
33 }
34 return &APIServer{r, serv, kratosAddr}
35}
36
37func (s *APIServer) Start() error {
38 s.r.Path("/identities").Methods(http.MethodPost).HandlerFunc(s.identityCreate)
39 return s.serv.ListenAndServe()
40}
41
giodcd9fef2024-09-26 14:42:59 +020042type kratosIdentityCreateReq struct {
43 Credentials struct {
44 Password struct {
45 Config struct {
46 Password string `json:"password"`
47 } `json:"config"`
48 } `json:"password"`
49 } `json:"credentials"`
50 SchemaID string `json:"schema_id"`
51 State string `json:"state"`
52 Traits struct {
53 Username string `json:"username"`
54 } `json:"traits"`
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040055}
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040056
57type identityCreateReq struct {
58 Username string `json:"username,omitempty"`
59 Password string `json:"password,omitempty"`
60}
61
DTabidze52593392024-03-08 12:53:20 +040062func extractKratosErrorMessage(errResp ErrorResponse) []ValidationError {
63 var errors []ValidationError
64 switch errResp.Error.Status {
65 case "Conflict":
66 errors = append(errors, ValidationError{"username", "Username is not available."})
67 case "Bad Request":
68 errors = append(errors, ValidationError{"username", "Username is less than 3 characters."})
69 default:
70 errors = append(errors, ValidationError{"username", "Unexpexted Error."})
71 }
72 return errors
73}
74
75type ValidationError struct {
76 Field string `json:"field"`
77 Message string `json:"message"`
78}
79
80type CombinedErrors struct {
81 Errors []ValidationError `json:"errors"`
82}
83
84func validateUsername(username string) []ValidationError {
85 var errors []ValidationError
86 if len(username) < 3 {
87 errors = append(errors, ValidationError{"username", "Username must be at least 3 characters long."})
88 }
89 // TODO other validations
90 return errors
91}
92
93func validatePassword(password string) []ValidationError {
94 var errors []ValidationError
giodcd9fef2024-09-26 14:42:59 +020095 if len(password) < 20 {
96 errors = append(errors, ValidationError{"password", "Password must be at least 20 characters long."})
97 }
98 digit := false
99 lowerCase := false
100 upperCase := false
101 special := false
102 for _, c := range password {
103 if unicode.IsDigit(c) {
104 digit = true
105 } else if unicode.IsLower(c) {
106 lowerCase = true
107 } else if unicode.IsUpper(c) {
108 upperCase = true
109 } else if strings.Contains(" !\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~", string(c)) {
110 special = true
111 }
112 }
113 if !digit || !lowerCase || !upperCase || !special {
114 errors = append(errors, ValidationError{"password", "Password must contain at least one ditig, lower/upper and special character"})
DTabidze52593392024-03-08 12:53:20 +0400115 }
116 // TODO other validations
117 return errors
118}
119
120func replyWithErrors(w http.ResponseWriter, errors []ValidationError) {
121 response := CombinedErrors{Errors: errors}
122 w.Header().Set("Content-Type", "application/json")
123 w.WriteHeader(http.StatusBadRequest)
124 if err := json.NewEncoder(w).Encode(response); err != nil {
125 http.Error(w, "failed to decode", http.StatusInternalServerError)
126 return
127 }
128}
129
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400130func (s *APIServer) identityCreate(w http.ResponseWriter, r *http.Request) {
131 var req identityCreateReq
132 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
133 http.Error(w, "request can not be parsed", http.StatusBadRequest)
134 return
135 }
DTabidze52593392024-03-08 12:53:20 +0400136 usernameErrors := validateUsername(req.Username)
137 passwordErrors := validatePassword(req.Password)
138 allErrors := append(usernameErrors, passwordErrors...)
139 if len(allErrors) > 0 {
140 replyWithErrors(w, allErrors)
141 return
142 }
giodcd9fef2024-09-26 14:42:59 +0200143 var kreq kratosIdentityCreateReq
144 kreq.Credentials.Password.Config.Password = req.Password
145 kreq.SchemaID = "user"
146 kreq.State = "active"
147 kreq.Traits.Username = req.Username
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400148 var buf bytes.Buffer
giodcd9fef2024-09-26 14:42:59 +0200149 if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
150 http.Error(w, err.Error(), http.StatusInternalServerError)
151 return
152 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400153 resp, err := http.Post(s.identitiesEndpoint(), "application/json", &buf)
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400154 if err != nil {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400155 http.Error(w, "failed", http.StatusInternalServerError)
156 return
DTabidze52593392024-03-08 12:53:20 +0400157 }
158 if resp.StatusCode != http.StatusCreated {
159 var e ErrorResponse
160 if err := json.NewDecoder(resp.Body).Decode(&e); err != nil {
161 http.Error(w, "failed to decode", http.StatusInternalServerError)
162 return
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400163 }
DTabidze52593392024-03-08 12:53:20 +0400164 errorMessages := extractKratosErrorMessage(e)
165 replyWithErrors(w, errorMessages)
166 return
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400167 }
168}
169
170func (s *APIServer) identitiesEndpoint() string {
171 return fmt.Sprintf("%s/admin/identities", s.kratosAddr)
172}