blob: 3de264c9ae5af0e86b31dd48ab5f9272664fad1a [file] [log] [blame]
giolekva603e73a2021-10-22 14:46:45 +04001package main
2
3import (
4 "bytes"
giolekvadd750802021-11-07 13:24:21 +04005 "crypto/tls"
giolekva603e73a2021-10-22 14:46:45 +04006 "embed"
7 "encoding/json"
8 "errors"
9 "flag"
10 "fmt"
11 "html/template"
12 "io"
13 "io/ioutil"
14 "log"
15 "net/http"
16 "net/http/cookiejar"
17 "net/url"
18
19 "github.com/gorilla/mux"
20 "github.com/itaysk/regogo"
21)
22
23var port = flag.Int("port", 8080, "Port to listen on")
24var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
giolekva788dc6e2021-10-25 20:40:53 +040025var hydra = flag.String("hydra", "hydra.pcloud", "Hydra admin server address")
giolekvadd750802021-11-07 13:24:21 +040026var emailDomain = flag.String("email-domain", "lekva.me", "Email domain")
giolekva603e73a2021-10-22 14:46:45 +040027
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040028var apiPort = flag.Int("api-port", 8081, "API Port to listen on")
29var kratosAPI = flag.String("kratos-api", "", "Kratos API address")
30
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040031var enableRegistration = flag.Bool("enable-registration", false, "If true account registration will be enabled")
32
giolekva603e73a2021-10-22 14:46:45 +040033var ErrNotLoggedIn = errors.New("Not logged in")
34
35//go:embed templates/*
36var tmpls embed.FS
37
giolekva47031752021-11-12 14:34:33 +040038//go:embed static
39var static embed.FS
40
giolekva603e73a2021-10-22 14:46:45 +040041type Templates struct {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040042 WhoAmI *template.Template
43 Register *template.Template
44 Login *template.Template
45 Consent *template.Template
giolekva603e73a2021-10-22 14:46:45 +040046}
47
48func ParseTemplates(fs embed.FS) (*Templates, error) {
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040049 base, err := template.ParseFS(fs, "templates/base.html")
giolekva788dc6e2021-10-25 20:40:53 +040050 if err != nil {
51 return nil, err
52 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040053 parse := func(path string) (*template.Template, error) {
54 if b, err := base.Clone(); err != nil {
55 return nil, err
56 } else {
57 return b.ParseFS(fs, path)
58 }
59 }
60 whoami, err := parse("templates/whoami.html")
giolekva603e73a2021-10-22 14:46:45 +040061 if err != nil {
62 return nil, err
63 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040064 register, err := parse("templates/register.html")
giolekva603e73a2021-10-22 14:46:45 +040065 if err != nil {
66 return nil, err
67 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040068 login, err := parse("templates/login.html")
giolekva603e73a2021-10-22 14:46:45 +040069 if err != nil {
70 return nil, err
71 }
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +040072 consent, err := parse("templates/consent.html")
73 if err != nil {
74 return nil, err
75 }
76 return &Templates{whoami, register, login, consent}, nil
giolekva603e73a2021-10-22 14:46:45 +040077}
78
79type Server struct {
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040080 r *mux.Router
81 serv *http.Server
82 kratos string
83 hydra *HydraClient
84 tmpls *Templates
85 enableRegistration bool
giolekva603e73a2021-10-22 14:46:45 +040086}
87
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040088func NewServer(port int, kratos string, hydra *HydraClient, tmpls *Templates, enableRegistration bool) *Server {
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040089 r := mux.NewRouter()
90 serv := &http.Server{
91 Addr: fmt.Sprintf(":%d", port),
92 Handler: r,
93 }
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +040094 return &Server{r, serv, kratos, hydra, tmpls, enableRegistration}
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +040095}
96
giolekva47031752021-11-12 14:34:33 +040097func cacheControlWrapper(h http.Handler) http.Handler {
98 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99 // TODO(giolekva): enable caching
100 // w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
101 h.ServeHTTP(w, r)
102 })
103}
104
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400105func (s *Server) Start() error {
giolekva47031752021-11-12 14:34:33 +0400106 var staticFS = http.FS(static)
107 fs := http.FileServer(staticFS)
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400108 s.r.PathPrefix("/static/").Handler(cacheControlWrapper(fs))
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400109 if s.enableRegistration {
110 s.r.Path("/register").Methods(http.MethodGet).HandlerFunc(s.registerInitiate)
111 s.r.Path("/register").Methods(http.MethodPost).HandlerFunc(s.register)
112 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400113 s.r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
114 s.r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
115 s.r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
116 s.r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
117 s.r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
118 s.r.Path("/").HandlerFunc(s.whoami)
119 return s.serv.ListenAndServe()
giolekva603e73a2021-10-22 14:46:45 +0400120}
121
122func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
123 jar, err := cookiejar.New(nil)
124 if err != nil {
125 return "", err
126 }
127 client := &http.Client{
128 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400129 Transport: &http.Transport{
130 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
131 },
giolekva603e73a2021-10-22 14:46:45 +0400132 }
giolekvadd750802021-11-07 13:24:21 +0400133 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400134 if err != nil {
135 return "", err
136 }
137 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400138 resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
giolekva603e73a2021-10-22 14:46:45 +0400139 if err != nil {
140 return "", err
141 }
142 respBody, err := ioutil.ReadAll(resp.Body)
143 if err != nil {
144 return "", err
145 }
146 token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
147 if err != nil {
148 return "", err
149 }
150 return token.String(), nil
151}
152
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400153func (s *Server) registerInitiate(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400154 if err := r.ParseForm(); err != nil {
155 http.Error(w, err.Error(), http.StatusInternalServerError)
156 return
157 }
158 flow, ok := r.Form["flow"]
159 if !ok {
160 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
161 return
162 }
163 csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
164 if err != nil {
165 http.Error(w, err.Error(), http.StatusInternalServerError)
166 return
167 }
giolekva603e73a2021-10-22 14:46:45 +0400168 w.Header().Set("Content-Type", "text/html")
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400169 if err := s.tmpls.Register.Execute(w, csrfToken); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400170 http.Error(w, err.Error(), http.StatusInternalServerError)
171 return
172 }
173}
174
175type regReq struct {
176 CSRFToken string `json:"csrf_token"`
177 Method string `json:"method"`
178 Password string `json:"password"`
179 Traits regReqTraits `json:"traits"`
180}
181
182type regReqTraits struct {
183 Username string `json:"username"`
184}
185
Giorgi Lekveishvili58cb1482023-12-04 12:33:49 +0400186func (s *Server) register(w http.ResponseWriter, r *http.Request) {
giolekva603e73a2021-10-22 14:46:45 +0400187 if err := r.ParseForm(); err != nil {
188 http.Error(w, err.Error(), http.StatusInternalServerError)
189 return
190 }
191 flow, ok := r.Form["flow"]
192 if !ok {
193 http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
194 return
195 }
196 req := regReq{
197 CSRFToken: r.FormValue("csrf_token"),
198 Method: "password",
199 Password: r.FormValue("password"),
200 Traits: regReqTraits{
201 Username: r.FormValue("username"),
202 },
203 }
204 var reqBody bytes.Buffer
205 if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
206 http.Error(w, err.Error(), http.StatusInternalServerError)
207 return
208 }
209 if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
210 http.Error(w, err.Error(), http.StatusInternalServerError)
211 return
212 } else {
213 for _, c := range resp.Cookies() {
214 http.SetCookie(w, c)
215 }
216 http.Redirect(w, r, "/", http.StatusSeeOther)
217 }
218}
219
220// Login flow
221
222func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
223 if err := r.ParseForm(); err != nil {
224 http.Error(w, err.Error(), http.StatusInternalServerError)
225 return
226 }
giolekva788dc6e2021-10-25 20:40:53 +0400227 if challenge, ok := r.Form["login_challenge"]; ok {
228 // TODO(giolekva): encrypt
229 http.SetCookie(w, &http.Cookie{
230 Name: "login_challenge",
231 Value: challenge[0],
232 HttpOnly: true,
233 })
234 } else {
235 // http.SetCookie(w, &http.Cookie{
236 // Name: "login_challenge",
237 // Value: "",
238 // Expires: time.Unix(0, 0),
239 // HttpOnly: true,
240 // })
241 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400242 returnTo := r.Form.Get("return_to")
giolekva603e73a2021-10-22 14:46:45 +0400243 flow, ok := r.Form["flow"]
244 if !ok {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400245 addr := s.kratos + "/self-service/login/browser"
246 if returnTo != "" {
247 addr += fmt.Sprintf("?return_to=%s", returnTo)
248 }
249 http.Redirect(w, r, addr, http.StatusSeeOther)
giolekva603e73a2021-10-22 14:46:45 +0400250 return
251 }
252 csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
253 if err != nil {
254 http.Error(w, err.Error(), http.StatusInternalServerError)
255 return
256 }
giolekva603e73a2021-10-22 14:46:45 +0400257 w.Header().Set("Content-Type", "text/html")
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400258 if err := s.tmpls.Login.Execute(w, map[string]any{
259 "csrfToken": csrfToken,
260 "enableRegistration": s.enableRegistration,
261 }); err != nil {
giolekva603e73a2021-10-22 14:46:45 +0400262 http.Error(w, err.Error(), http.StatusInternalServerError)
263 return
264 }
265}
266
267type loginReq struct {
268 CSRFToken string `json:"csrf_token"`
269 Method string `json:"method"`
270 Password string `json:"password"`
271 Username string `json:"password_identifier"`
272}
273
274func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
275 jar, err := cookiejar.New(nil)
276 if err != nil {
277 return nil, err
278 }
279 client := &http.Client{
280 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400281 Transport: &http.Transport{
282 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
283 },
giolekva603e73a2021-10-22 14:46:45 +0400284 }
giolekvadd750802021-11-07 13:24:21 +0400285 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
giolekva603e73a2021-10-22 14:46:45 +0400286 if err != nil {
287 return nil, err
288 }
289 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400290 resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
giolekva603e73a2021-10-22 14:46:45 +0400291 if err != nil {
292 return nil, err
293 }
294 return resp, nil
295}
296
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400297func postFormToKratos(flowType, flow string, cookies []*http.Cookie, data url.Values) (*http.Response, error) {
298 jar, err := cookiejar.New(nil)
299 if err != nil {
300 return nil, err
301 }
302 client := &http.Client{
303 Jar: jar,
304 Transport: &http.Transport{
305 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
306 },
307 CheckRedirect: func(req *http.Request, via []*http.Request) error {
308 return http.ErrUseLastResponse
309 },
310 }
311 b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
312 if err != nil {
313 return nil, err
314 }
315 client.Jar.SetCookies(b, cookies)
316 resp, err := client.PostForm(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), data)
317 if err != nil {
318 return nil, err
319 }
320 return resp, nil
321}
322
giolekva603e73a2021-10-22 14:46:45 +0400323type logoutResp struct {
324 LogoutURL string `json:"logout_url"`
325}
326
327func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
328 jar, err := cookiejar.New(nil)
329 if err != nil {
330 return "", err
331 }
332 client := &http.Client{
333 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400334 Transport: &http.Transport{
335 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
336 },
giolekva603e73a2021-10-22 14:46:45 +0400337 }
giolekvadd750802021-11-07 13:24:21 +0400338 b, err := url.Parse(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400339 if err != nil {
340 return "", err
341 }
342 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400343 resp, err := client.Get(*kratos + "/self-service/logout/browser")
giolekva603e73a2021-10-22 14:46:45 +0400344 if err != nil {
345 return "", err
346 }
347 var lr logoutResp
348 if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
349 return "", err
350 }
351 return lr.LogoutURL, nil
352}
353
354func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
355 jar, err := cookiejar.New(nil)
356 if err != nil {
357 return "", err
358 }
359 client := &http.Client{
360 Jar: jar,
giolekvadd750802021-11-07 13:24:21 +0400361 Transport: &http.Transport{
362 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
363 },
giolekva603e73a2021-10-22 14:46:45 +0400364 }
giolekvadd750802021-11-07 13:24:21 +0400365 b, err := url.Parse(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400366 if err != nil {
367 return "", err
368 }
369 client.Jar.SetCookies(b, cookies)
giolekvadd750802021-11-07 13:24:21 +0400370 resp, err := client.Get(*kratos + "/sessions/whoami")
giolekva603e73a2021-10-22 14:46:45 +0400371 if err != nil {
372 return "", err
373 }
374 respBody, err := ioutil.ReadAll(resp.Body)
375 if err != nil {
376 return "", err
377 }
378 username, err := regogo.Get(string(respBody), "input.identity.traits.username")
379 if err != nil {
380 return "", err
381 }
382 if username.String() == "" {
383 return "", ErrNotLoggedIn
384 }
385 return username.String(), nil
386
387}
388
giolekva788dc6e2021-10-25 20:40:53 +0400389func extractError(r io.Reader) error {
390 respBody, err := ioutil.ReadAll(r)
391 if err != nil {
392 return err
393 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400394 fmt.Printf("++ %s\n", respBody)
giolekva788dc6e2021-10-25 20:40:53 +0400395 t, err := regogo.Get(string(respBody), "input.ui.messages[0].type")
396 if err != nil {
397 return err
398 }
399 if t.String() == "error" {
400 message, err := regogo.Get(string(respBody), "input.ui.messages[0].text")
401 if err != nil {
402 return err
403 }
404 return errors.New(message.String())
405 }
406 return nil
407}
408
giolekva603e73a2021-10-22 14:46:45 +0400409func (s *Server) login(w http.ResponseWriter, r *http.Request) {
410 if err := r.ParseForm(); err != nil {
411 http.Error(w, err.Error(), http.StatusInternalServerError)
412 return
413 }
414 flow, ok := r.Form["flow"]
415 if !ok {
416 http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
417 return
418 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400419 req := url.Values{
420 "csrf_token": []string{r.FormValue("csrf_token")},
421 "method": []string{"password"},
422 "password": []string{r.FormValue("password")},
423 "identifier": []string{r.FormValue("username")},
giolekva603e73a2021-10-22 14:46:45 +0400424 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400425 resp, err := postFormToKratos("login", flow[0], r.Cookies(), req)
426 fmt.Printf("--- %d\n", resp.StatusCode)
427 var vv bytes.Buffer
428 io.Copy(&vv, resp.Body)
429 fmt.Println(vv.String())
giolekva788dc6e2021-10-25 20:40:53 +0400430 if err != nil {
giolekvaeb590282021-10-22 17:31:40 +0400431 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
giolekva788dc6e2021-10-25 20:40:53 +0400432 redirectTo, err := s.hydra.LoginRejectChallenge(challenge.Value, err.Error())
giolekvaeb590282021-10-22 17:31:40 +0400433 if err != nil {
434 http.Error(w, err.Error(), http.StatusInternalServerError)
435 return
436 }
giolekva788dc6e2021-10-25 20:40:53 +0400437 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
438 return
giolekvaeb590282021-10-22 17:31:40 +0400439 }
giolekva788dc6e2021-10-25 20:40:53 +0400440 http.Error(w, err.Error(), http.StatusInternalServerError)
441 return
giolekva603e73a2021-10-22 14:46:45 +0400442 }
giolekva788dc6e2021-10-25 20:40:53 +0400443 for _, c := range resp.Cookies() {
444 http.SetCookie(w, c)
445 }
446 if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
447 username, err := getWhoAmIFromKratos(resp.Cookies())
448 if err != nil {
449 http.Error(w, err.Error(), http.StatusInternalServerError)
450 return
451 }
452 redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
453 if err != nil {
454 http.Error(w, err.Error(), http.StatusInternalServerError)
455 return
456 }
457 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
458 return
459 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400460 if resp.StatusCode == http.StatusSeeOther {
461 http.Redirect(w, r, resp.Header.Get("Location"), http.StatusSeeOther)
462 } else {
463 http.Redirect(w, r, "/", http.StatusSeeOther)
464 }
giolekva603e73a2021-10-22 14:46:45 +0400465}
466
467func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
468 if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
469 http.Error(w, err.Error(), http.StatusInternalServerError)
470 return
471 } else {
472 http.Redirect(w, r, logoutURL, http.StatusSeeOther)
473 }
474}
475
476func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
477 if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
478 if errors.Is(err, ErrNotLoggedIn) {
479 http.Redirect(w, r, "/login", http.StatusSeeOther)
480 return
481 }
482 http.Error(w, err.Error(), http.StatusInternalServerError)
483 } else {
484 if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
485 http.Error(w, err.Error(), http.StatusInternalServerError)
486 }
487 }
488}
489
giolekva788dc6e2021-10-25 20:40:53 +0400490// TODO(giolekva): verify if logged in
491func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
492 if err := r.ParseForm(); err != nil {
493 http.Error(w, err.Error(), http.StatusBadRequest)
494 return
495 }
496 challenge, ok := r.Form["consent_challenge"]
497 if !ok {
498 http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
499 return
500 }
501 consent, err := s.hydra.GetConsentChallenge(challenge[0])
502 if err != nil {
503 http.Error(w, err.Error(), http.StatusInternalServerError)
504 return
505 }
506 w.Header().Set("Content-Type", "text/html")
507 if err := s.tmpls.Consent.Execute(w, consent.RequestedScopes); err != nil {
508 http.Error(w, err.Error(), http.StatusInternalServerError)
509 return
510 }
511}
512
513func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
514 if err := r.ParseForm(); err != nil {
515 http.Error(w, err.Error(), http.StatusBadRequest)
516 return
517 }
518 username, err := getWhoAmIFromKratos(r.Cookies())
519 if err != nil {
520 http.Error(w, err.Error(), http.StatusInternalServerError)
521 return
522 }
523 if _, accepted := r.Form["allow"]; accepted {
524 acceptedScopes, _ := r.Form["scope"]
525 idToken := map[string]string{
526 "username": username,
giolekvadd750802021-11-07 13:24:21 +0400527 "email": username + "@" + *emailDomain,
giolekva788dc6e2021-10-25 20:40:53 +0400528 }
529 if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
530 http.Error(w, err.Error(), http.StatusInternalServerError)
531 } else {
532 http.Redirect(w, r, redirectTo, http.StatusSeeOther)
533 }
534 return
535 } else {
536 // TODO(giolekva): implement rejection logic
537 }
538}
539
giolekva603e73a2021-10-22 14:46:45 +0400540func main() {
541 flag.Parse()
542 t, err := ParseTemplates(tmpls)
543 if err != nil {
544 log.Fatal(err)
545 }
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400546 go func() {
547 s := NewAPIServer(*apiPort, *kratosAPI)
548 log.Fatal(s.Start())
549 }()
550 func() {
551 s := NewServer(
552 *port,
553 *kratos,
554 NewHydraClient(*hydra),
555 t,
Giorgi Lekveishvilid76414e2023-12-21 13:30:23 +0400556 *enableRegistration,
Giorgi Lekveishvilifedd0062023-12-21 10:52:49 +0400557 )
558 log.Fatal(s.Start())
559 }()
giolekva603e73a2021-10-22 14:46:45 +0400560}