blob: b8c5badb1a1667ef3ff61c0be4a99fd61e7aec97 [file] [log] [blame]
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +04001package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
giodd213152024-09-27 11:26:59 +02007 "io"
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +04008 "net/http"
giodd213152024-09-27 11:26:59 +02009 "net/url"
giodcd9fef2024-09-26 14:42:59 +020010 "strings"
11 "unicode"
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040012
13 "github.com/gorilla/mux"
14)
15
16type APIServer struct {
17 r *mux.Router
18 serv *http.Server
19 kratosAddr string
20}
21
DTabidze52593392024-03-08 12:53:20 +040022type ErrorResponse struct {
23 Error struct {
24 Code int `json:"code"`
25 Status string `json:"status"`
26 Message string `json:"message"`
27 } `json:"error"`
28}
29
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040030func NewAPIServer(port int, kratosAddr string) *APIServer {
31 r := mux.NewRouter()
32 serv := &http.Server{
33 Addr: fmt.Sprintf(":%d", port),
34 Handler: r,
35 }
36 return &APIServer{r, serv, kratosAddr}
37}
38
39func (s *APIServer) Start() error {
40 s.r.Path("/identities").Methods(http.MethodPost).HandlerFunc(s.identityCreate)
41 return s.serv.ListenAndServe()
42}
43
giodcd9fef2024-09-26 14:42:59 +020044type kratosIdentityCreateReq struct {
45 Credentials struct {
46 Password struct {
47 Config struct {
48 Password string `json:"password"`
49 } `json:"config"`
50 } `json:"password"`
51 } `json:"credentials"`
52 SchemaID string `json:"schema_id"`
53 State string `json:"state"`
54 Traits struct {
55 Username string `json:"username"`
56 } `json:"traits"`
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040057}
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040058
59type identityCreateReq struct {
60 Username string `json:"username,omitempty"`
61 Password string `json:"password,omitempty"`
62}
63
DTabidze52593392024-03-08 12:53:20 +040064func extractKratosErrorMessage(errResp ErrorResponse) []ValidationError {
65 var errors []ValidationError
66 switch errResp.Error.Status {
67 case "Conflict":
68 errors = append(errors, ValidationError{"username", "Username is not available."})
69 case "Bad Request":
70 errors = append(errors, ValidationError{"username", "Username is less than 3 characters."})
71 default:
72 errors = append(errors, ValidationError{"username", "Unexpexted Error."})
73 }
74 return errors
75}
76
77type ValidationError struct {
78 Field string `json:"field"`
79 Message string `json:"message"`
80}
81
82type CombinedErrors struct {
83 Errors []ValidationError `json:"errors"`
84}
85
86func validateUsername(username string) []ValidationError {
87 var errors []ValidationError
88 if len(username) < 3 {
89 errors = append(errors, ValidationError{"username", "Username must be at least 3 characters long."})
90 }
91 // TODO other validations
92 return errors
93}
94
95func validatePassword(password string) []ValidationError {
96 var errors []ValidationError
giodcd9fef2024-09-26 14:42:59 +020097 if len(password) < 20 {
98 errors = append(errors, ValidationError{"password", "Password must be at least 20 characters long."})
99 }
100 digit := false
101 lowerCase := false
102 upperCase := false
103 special := false
104 for _, c := range password {
105 if unicode.IsDigit(c) {
106 digit = true
107 } else if unicode.IsLower(c) {
108 lowerCase = true
109 } else if unicode.IsUpper(c) {
110 upperCase = true
111 } else if strings.Contains(" !\"#$%&'()*+,-./:;<=>?@[\\]^_{|}~", string(c)) {
112 special = true
113 }
114 }
115 if !digit || !lowerCase || !upperCase || !special {
giodd213152024-09-27 11:26:59 +0200116 errors = append(errors, ValidationError{"password", "Password must contain at least one digit, lower&upper case and special characters"})
DTabidze52593392024-03-08 12:53:20 +0400117 }
118 // TODO other validations
119 return errors
120}
121
122func replyWithErrors(w http.ResponseWriter, errors []ValidationError) {
123 response := CombinedErrors{Errors: errors}
124 w.Header().Set("Content-Type", "application/json")
125 w.WriteHeader(http.StatusBadRequest)
126 if err := json.NewEncoder(w).Encode(response); err != nil {
127 http.Error(w, "failed to decode", http.StatusInternalServerError)
128 return
129 }
130}
131
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400132func (s *APIServer) identityCreate(w http.ResponseWriter, r *http.Request) {
133 var req identityCreateReq
134 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
135 http.Error(w, "request can not be parsed", http.StatusBadRequest)
136 return
137 }
DTabidze52593392024-03-08 12:53:20 +0400138 usernameErrors := validateUsername(req.Username)
139 passwordErrors := validatePassword(req.Password)
140 allErrors := append(usernameErrors, passwordErrors...)
141 if len(allErrors) > 0 {
142 replyWithErrors(w, allErrors)
143 return
144 }
giodcd9fef2024-09-26 14:42:59 +0200145 var kreq kratosIdentityCreateReq
146 kreq.Credentials.Password.Config.Password = req.Password
147 kreq.SchemaID = "user"
148 kreq.State = "active"
149 kreq.Traits.Username = req.Username
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400150 var buf bytes.Buffer
giodcd9fef2024-09-26 14:42:59 +0200151 if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
152 http.Error(w, err.Error(), http.StatusInternalServerError)
153 return
154 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400155 resp, err := http.Post(s.identitiesEndpoint(), "application/json", &buf)
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400156 if err != nil {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400157 http.Error(w, "failed", http.StatusInternalServerError)
158 return
DTabidze52593392024-03-08 12:53:20 +0400159 }
160 if resp.StatusCode != http.StatusCreated {
161 var e ErrorResponse
162 if err := json.NewDecoder(resp.Body).Decode(&e); err != nil {
163 http.Error(w, "failed to decode", http.StatusInternalServerError)
164 return
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400165 }
DTabidze52593392024-03-08 12:53:20 +0400166 errorMessages := extractKratosErrorMessage(e)
167 replyWithErrors(w, errorMessages)
168 return
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400169 }
170}
171
giodd213152024-09-27 11:26:59 +0200172type changePasswordReq struct {
173 Id string `json:"id,omitempty"`
174 Username string `json:"username,omitempty"`
175 Password string `json:"password,omitempty"`
176}
177
178func (s *APIServer) apiPasswordChange(id, username, password string) ([]ValidationError, error) {
179 var usernameErrors []ValidationError
180 passwordErrors := validatePassword(password)
181 allErrors := append(usernameErrors, passwordErrors...)
182 if len(allErrors) > 0 {
183 return allErrors, nil
184 }
185 var kreq kratosIdentityCreateReq
186 kreq.Credentials.Password.Config.Password = password
187 kreq.SchemaID = "user"
188 kreq.State = "active"
189 kreq.Traits.Username = username
190 var buf bytes.Buffer
191 if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
192 return nil, err
193 }
194 c := http.Client{}
195 addr, err := url.Parse(s.identityEndpoint(id))
196 if err != nil {
197 return nil, err
198 }
199 hreq := &http.Request{
200 Method: http.MethodPut,
201 URL: addr,
202 Header: http.Header{"Content-Type": []string{"application/json"}},
203 Body: io.NopCloser(&buf),
204 }
205 resp, err := c.Do(hreq)
206 if err != nil {
207 return nil, err
208 }
209 if resp.StatusCode != http.StatusOK {
210 var buf bytes.Buffer
211 io.Copy(&buf, resp.Body)
212 respS := buf.String()
213 fmt.Printf("PASSWORD CHANGE ERROR: %s\n", respS)
214 var e ErrorResponse
215 if err := json.NewDecoder(bytes.NewReader([]byte(respS))).Decode(&e); err != nil {
216 return nil, err
217 }
218 return extractKratosErrorMessage(e), nil
219 }
220 return nil, nil
221}
222
223func (s *APIServer) passwordChange(w http.ResponseWriter, r *http.Request) {
224 var req changePasswordReq
225 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
226 http.Error(w, err.Error(), http.StatusBadRequest)
227 return
228 }
229 if verr, err := s.apiPasswordChange(req.Id, req.Username, req.Password); err != nil {
230 http.Error(w, err.Error(), http.StatusInternalServerError)
231 } else if len(verr) > 0 {
232 replyWithErrors(w, verr)
233 }
234}
235
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400236func (s *APIServer) identitiesEndpoint() string {
237 return fmt.Sprintf("%s/admin/identities", s.kratosAddr)
238}
giodd213152024-09-27 11:26:59 +0200239
240func (s *APIServer) identityEndpoint(id string) string {
241 return fmt.Sprintf("%s/admin/identities/%s", s.kratosAddr, id)
242}