blob: cced90d1a777012d293220516227127515ad6656 [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
gio134be722025-07-20 19:01:17 +0400132type identityCreateResp struct {
133 Id string `json:"id"`
134}
135
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400136func (s *APIServer) identityCreate(w http.ResponseWriter, r *http.Request) {
137 var req identityCreateReq
138 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
139 http.Error(w, "request can not be parsed", http.StatusBadRequest)
140 return
141 }
DTabidze52593392024-03-08 12:53:20 +0400142 usernameErrors := validateUsername(req.Username)
143 passwordErrors := validatePassword(req.Password)
144 allErrors := append(usernameErrors, passwordErrors...)
145 if len(allErrors) > 0 {
146 replyWithErrors(w, allErrors)
147 return
148 }
giodcd9fef2024-09-26 14:42:59 +0200149 var kreq kratosIdentityCreateReq
150 kreq.Credentials.Password.Config.Password = req.Password
151 kreq.SchemaID = "user"
152 kreq.State = "active"
153 kreq.Traits.Username = req.Username
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400154 var buf bytes.Buffer
giodcd9fef2024-09-26 14:42:59 +0200155 if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
156 http.Error(w, err.Error(), http.StatusInternalServerError)
157 return
158 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400159 resp, err := http.Post(s.identitiesEndpoint(), "application/json", &buf)
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400160 if err != nil {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400161 http.Error(w, "failed", http.StatusInternalServerError)
162 return
DTabidze52593392024-03-08 12:53:20 +0400163 }
gio134be722025-07-20 19:01:17 +0400164 if resp.StatusCode == http.StatusCreated {
165 var idResp identityCreateResp
166 if err := json.NewDecoder(resp.Body).Decode(&idResp); err != nil {
DTabidze52593392024-03-08 12:53:20 +0400167 http.Error(w, "failed to decode", http.StatusInternalServerError)
168 return
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400169 }
gio134be722025-07-20 19:01:17 +0400170 if err := json.NewEncoder(w).Encode(idResp); err != nil {
171 http.Error(w, "failed to decode", http.StatusInternalServerError)
172 return
173 }
DTabidze52593392024-03-08 12:53:20 +0400174 return
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400175 }
gio134be722025-07-20 19:01:17 +0400176 var e ErrorResponse
177 if err := json.NewDecoder(resp.Body).Decode(&e); err != nil {
178 http.Error(w, "failed to decode", http.StatusInternalServerError)
179 return
180 }
181 errorMessages := extractKratosErrorMessage(e)
182 replyWithErrors(w, errorMessages)
183 return
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400184}
185
giodd213152024-09-27 11:26:59 +0200186type changePasswordReq struct {
187 Id string `json:"id,omitempty"`
188 Username string `json:"username,omitempty"`
189 Password string `json:"password,omitempty"`
190}
191
192func (s *APIServer) apiPasswordChange(id, username, password string) ([]ValidationError, error) {
193 var usernameErrors []ValidationError
194 passwordErrors := validatePassword(password)
195 allErrors := append(usernameErrors, passwordErrors...)
196 if len(allErrors) > 0 {
197 return allErrors, nil
198 }
199 var kreq kratosIdentityCreateReq
200 kreq.Credentials.Password.Config.Password = password
201 kreq.SchemaID = "user"
202 kreq.State = "active"
203 kreq.Traits.Username = username
204 var buf bytes.Buffer
205 if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
206 return nil, err
207 }
208 c := http.Client{}
209 addr, err := url.Parse(s.identityEndpoint(id))
210 if err != nil {
211 return nil, err
212 }
213 hreq := &http.Request{
214 Method: http.MethodPut,
215 URL: addr,
216 Header: http.Header{"Content-Type": []string{"application/json"}},
217 Body: io.NopCloser(&buf),
218 }
219 resp, err := c.Do(hreq)
220 if err != nil {
221 return nil, err
222 }
223 if resp.StatusCode != http.StatusOK {
224 var buf bytes.Buffer
225 io.Copy(&buf, resp.Body)
226 respS := buf.String()
227 fmt.Printf("PASSWORD CHANGE ERROR: %s\n", respS)
228 var e ErrorResponse
229 if err := json.NewDecoder(bytes.NewReader([]byte(respS))).Decode(&e); err != nil {
230 return nil, err
231 }
232 return extractKratosErrorMessage(e), nil
233 }
234 return nil, nil
235}
236
237func (s *APIServer) passwordChange(w http.ResponseWriter, r *http.Request) {
238 var req changePasswordReq
239 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
240 http.Error(w, err.Error(), http.StatusBadRequest)
241 return
242 }
243 if verr, err := s.apiPasswordChange(req.Id, req.Username, req.Password); err != nil {
244 http.Error(w, err.Error(), http.StatusInternalServerError)
245 } else if len(verr) > 0 {
246 replyWithErrors(w, verr)
247 }
248}
249
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400250func (s *APIServer) identitiesEndpoint() string {
251 return fmt.Sprintf("%s/admin/identities", s.kratosAddr)
252}
giodd213152024-09-27 11:26:59 +0200253
254func (s *APIServer) identityEndpoint(id string) string {
255 return fmt.Sprintf("%s/admin/identities/%s", s.kratosAddr, id)
256}