blob: e1b74d11baedb15874de6a6a85471521ee879663 [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"
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
DTabidze52593392024-03-08 12:53:20 +040075type CombinedErrors struct {
76 Errors []ValidationError `json:"errors"`
77}
78
DTabidze52593392024-03-08 12:53:20 +040079func replyWithErrors(w http.ResponseWriter, errors []ValidationError) {
80 response := CombinedErrors{Errors: errors}
81 w.Header().Set("Content-Type", "application/json")
82 w.WriteHeader(http.StatusBadRequest)
83 if err := json.NewEncoder(w).Encode(response); err != nil {
84 http.Error(w, "failed to decode", http.StatusInternalServerError)
85 return
86 }
87}
88
gio134be722025-07-20 19:01:17 +040089type identityCreateResp struct {
90 Id string `json:"id"`
91}
92
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040093func (s *APIServer) identityCreate(w http.ResponseWriter, r *http.Request) {
94 var req identityCreateReq
95 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
96 http.Error(w, "request can not be parsed", http.StatusBadRequest)
97 return
98 }
DTabidze52593392024-03-08 12:53:20 +040099 usernameErrors := validateUsername(req.Username)
100 passwordErrors := validatePassword(req.Password)
101 allErrors := append(usernameErrors, passwordErrors...)
102 if len(allErrors) > 0 {
103 replyWithErrors(w, allErrors)
104 return
105 }
giodcd9fef2024-09-26 14:42:59 +0200106 var kreq kratosIdentityCreateReq
107 kreq.Credentials.Password.Config.Password = req.Password
108 kreq.SchemaID = "user"
109 kreq.State = "active"
110 kreq.Traits.Username = req.Username
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400111 var buf bytes.Buffer
giodcd9fef2024-09-26 14:42:59 +0200112 if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
113 http.Error(w, err.Error(), http.StatusInternalServerError)
114 return
115 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400116 resp, err := http.Post(s.identitiesEndpoint(), "application/json", &buf)
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400117 if err != nil {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400118 http.Error(w, "failed", http.StatusInternalServerError)
119 return
DTabidze52593392024-03-08 12:53:20 +0400120 }
gio134be722025-07-20 19:01:17 +0400121 if resp.StatusCode == http.StatusCreated {
122 var idResp identityCreateResp
123 if err := json.NewDecoder(resp.Body).Decode(&idResp); err != nil {
DTabidze52593392024-03-08 12:53:20 +0400124 http.Error(w, "failed to decode", http.StatusInternalServerError)
125 return
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400126 }
gio134be722025-07-20 19:01:17 +0400127 if err := json.NewEncoder(w).Encode(idResp); err != nil {
128 http.Error(w, "failed to decode", http.StatusInternalServerError)
129 return
130 }
DTabidze52593392024-03-08 12:53:20 +0400131 return
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400132 }
gio134be722025-07-20 19:01:17 +0400133 var e ErrorResponse
134 if err := json.NewDecoder(resp.Body).Decode(&e); err != nil {
135 http.Error(w, "failed to decode", http.StatusInternalServerError)
136 return
137 }
138 errorMessages := extractKratosErrorMessage(e)
139 replyWithErrors(w, errorMessages)
140 return
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400141}
142
giodd213152024-09-27 11:26:59 +0200143type changePasswordReq struct {
144 Id string `json:"id,omitempty"`
145 Username string `json:"username,omitempty"`
146 Password string `json:"password,omitempty"`
147}
148
149func (s *APIServer) apiPasswordChange(id, username, password string) ([]ValidationError, error) {
150 var usernameErrors []ValidationError
151 passwordErrors := validatePassword(password)
152 allErrors := append(usernameErrors, passwordErrors...)
153 if len(allErrors) > 0 {
154 return allErrors, nil
155 }
156 var kreq kratosIdentityCreateReq
157 kreq.Credentials.Password.Config.Password = password
158 kreq.SchemaID = "user"
159 kreq.State = "active"
160 kreq.Traits.Username = username
161 var buf bytes.Buffer
162 if err := json.NewEncoder(&buf).Encode(kreq); err != nil {
163 return nil, err
164 }
165 c := http.Client{}
166 addr, err := url.Parse(s.identityEndpoint(id))
167 if err != nil {
168 return nil, err
169 }
170 hreq := &http.Request{
171 Method: http.MethodPut,
172 URL: addr,
173 Header: http.Header{"Content-Type": []string{"application/json"}},
174 Body: io.NopCloser(&buf),
175 }
176 resp, err := c.Do(hreq)
177 if err != nil {
178 return nil, err
179 }
180 if resp.StatusCode != http.StatusOK {
181 var buf bytes.Buffer
182 io.Copy(&buf, resp.Body)
183 respS := buf.String()
184 fmt.Printf("PASSWORD CHANGE ERROR: %s\n", respS)
185 var e ErrorResponse
186 if err := json.NewDecoder(bytes.NewReader([]byte(respS))).Decode(&e); err != nil {
187 return nil, err
188 }
189 return extractKratosErrorMessage(e), nil
190 }
191 return nil, nil
192}
193
194func (s *APIServer) passwordChange(w http.ResponseWriter, r *http.Request) {
195 var req changePasswordReq
196 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
197 http.Error(w, err.Error(), http.StatusBadRequest)
198 return
199 }
200 if verr, err := s.apiPasswordChange(req.Id, req.Username, req.Password); err != nil {
201 http.Error(w, err.Error(), http.StatusInternalServerError)
202 } else if len(verr) > 0 {
203 replyWithErrors(w, verr)
204 }
205}
206
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400207func (s *APIServer) identitiesEndpoint() string {
208 return fmt.Sprintf("%s/admin/identities", s.kratosAddr)
209}
giodd213152024-09-27 11:26:59 +0200210
211func (s *APIServer) identityEndpoint(id string) string {
212 return fmt.Sprintf("%s/admin/identities/%s", s.kratosAddr, id)
213}