blob: 85d7f6142cc857b8b0eddb5ac432f943760c6a3a [file] [log] [blame]
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +04001package welcome
2
3import (
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +04004 "bytes"
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +04005 "embed"
6 "encoding/json"
7 "fmt"
DTabidze52593392024-03-08 12:53:20 +04008 "html/template"
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +04009 "io"
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040010 "log"
11 "net/http"
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +040012 "net/url"
gio4784f8e2024-08-01 15:20:12 +040013 "os"
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040014
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040015 "github.com/gorilla/mux"
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040016
17 "github.com/giolekva/pcloud/core/installer"
gioe72b54f2024-04-22 10:44:41 +040018 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040019)
20
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040021//go:embed create-account.html
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040022var indexHtml []byte
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040023
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040024//go:embed create-account-success.html
25var successHtml []byte
26
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040027//go:embed static/*
28var staticAssets embed.FS
29
30type Server struct {
gio2728e402024-08-01 18:14:21 +040031 port int
32 repo soft.RepoIO
33 nsCreator installer.NamespaceCreator
34 hf installer.HelmFetcher
35 createAccountAddr string
36 loginAddr string
37 membershipsAddr string
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040038}
39
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040040func NewServer(
41 port int,
gioe72b54f2024-04-22 10:44:41 +040042 repo soft.RepoIO,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040043 nsCreator installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +040044 hf installer.HelmFetcher,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040045 createAccountAddr string,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040046 loginAddr string,
gio2728e402024-08-01 18:14:21 +040047 membershipsAddr string,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040048) *Server {
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040049 return &Server{
50 port,
51 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040052 nsCreator,
giof8843412024-05-22 16:38:05 +040053 hf,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040054 createAccountAddr,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040055 loginAddr,
gio2728e402024-08-01 18:14:21 +040056 membershipsAddr,
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040057 }
58}
59
60func (s *Server) Start() {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040061 r := mux.NewRouter()
gio09f8efa2024-06-10 22:35:24 +040062 r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040063 r.Path("/").Methods("POST").HandlerFunc(s.createAccount)
64 r.Path("/").Methods("GET").HandlerFunc(s.createAccountForm)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040065 http.Handle("/", r)
66 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040067}
68
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040069func (s *Server) createAccountForm(w http.ResponseWriter, r *http.Request) {
DTabidze52593392024-03-08 12:53:20 +040070 renderRegistrationForm(w, formData{})
71}
72
73type formData struct {
74 UsernameErrors []string
75 PasswordErrors []string
76 Data createAccountReq
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040077}
78
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040079type createAccountReq struct {
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040080 Username string `json:"username,omitempty"`
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040081 Password string `json:"password,omitempty"`
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040082 SecretToken string `json:"secretToken,omitempty"`
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040083}
84
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040085type apiCreateAccountReq struct {
86 Username string `json:"username,omitempty"`
87 Password string `json:"password,omitempty"`
88}
89
DTabidze52593392024-03-08 12:53:20 +040090type ValidationError struct {
91 Field string `json:"field"`
92 Message string `json:"message"`
93}
94
95type ErrorResponse struct {
96 Errors []ValidationError `json:"errors"`
97}
98
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +040099func getFormValue(v url.Values, name string) (string, error) {
100 items, ok := v[name]
101 if !ok || len(items) != 1 {
102 return "", fmt.Errorf("%s not found", name)
103 }
104 return items[0], nil
105}
106
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400107func extractReq(r *http.Request) (createAccountReq, error) {
108 var req createAccountReq
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400109 if err := func() error {
110 var err error
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400111 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400112 return err
113 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400114 if req.Username, err = getFormValue(r.PostForm, "username"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400115 return err
116 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400117 if req.Password, err = getFormValue(r.PostForm, "password"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400118 return err
119 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400120 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400121 return err
122 }
123 return nil
124 }(); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400125 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
126 return createAccountReq{}, err
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400127 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400128 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400129 return req, nil
130}
131
DTabidze52593392024-03-08 12:53:20 +0400132func renderRegistrationForm(w http.ResponseWriter, data formData) {
133 tmpl, err := template.New("create-account").Parse(string(indexHtml))
134 if err != nil {
135 http.Error(w, err.Error(), http.StatusInternalServerError)
136 return
137 }
138 if err := tmpl.Execute(w, data); err != nil {
139 http.Error(w, err.Error(), http.StatusInternalServerError)
140 return
141 }
142}
143
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400144func renderRegistrationSuccess(w http.ResponseWriter, loginAddr string) {
145 data := struct {
146 LoginAddr string
147 }{
148 LoginAddr: loginAddr,
149 }
150 tmpl, err := template.New("create-account-success").Parse(string(successHtml))
151 if err != nil {
152 http.Error(w, err.Error(), http.StatusInternalServerError)
153 return
154 }
155 if err := tmpl.Execute(w, data); err != nil {
156 http.Error(w, err.Error(), http.StatusInternalServerError)
157 return
158 }
159}
160
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400161func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400162 req, err := extractReq(r)
163 if err != nil {
164 http.Error(w, err.Error(), http.StatusInternalServerError)
165 return
166 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400167 {
168 var buf bytes.Buffer
169 cr := apiCreateAccountReq{req.Username, req.Password}
170 if err := json.NewEncoder(&buf).Encode(cr); err != nil {
171 http.Error(w, err.Error(), http.StatusInternalServerError)
172 return
173 }
174 resp, err := http.Post(s.createAccountAddr, "application/json", &buf)
175 if err != nil {
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400176 var respBody bytes.Buffer
177 if _, err := io.Copy(&respBody, resp.Body); err != nil {
178 http.Error(w, err.Error(), http.StatusInternalServerError)
179 }
180 respStr := respBody.String()
181 log.Println(respStr)
182 http.Error(w, respStr, http.StatusInternalServerError)
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400183 return
184 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400185 if resp.StatusCode != http.StatusOK {
DTabidze52593392024-03-08 12:53:20 +0400186 var errResponse ErrorResponse
187 if err := json.NewDecoder(resp.Body).Decode(&errResponse); err != nil {
188 http.Error(w, "Error Decoding JSON", http.StatusInternalServerError)
189 return
190 }
191 var usernameErrors, passwordErrors []string
192 for _, err := range errResponse.Errors {
193 if err.Field == "username" {
194 usernameErrors = append(usernameErrors, err.Message)
195 }
196 if err.Field == "password" {
197 passwordErrors = append(passwordErrors, err.Message)
198 }
199 }
200 renderRegistrationForm(w, formData{
201 usernameErrors,
202 passwordErrors,
203 req,
204 })
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400205 return
206 }
207 }
gio2728e402024-08-01 18:14:21 +0400208 if err := s.createUser(req.Username); err != nil {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400209 http.Error(w, err.Error(), http.StatusInternalServerError)
210 return
211 }
giof8843412024-05-22 16:38:05 +0400212 // TODO(gio): remove this once auto user sync is implemented
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400213 {
giof8843412024-05-22 16:38:05 +0400214 appManager, err := installer.NewAppManager(s.repo, s.nsCreator, nil, s.hf, "/apps")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400215 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400216 http.Error(w, err.Error(), http.StatusInternalServerError)
217 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400218 }
gio3cdee592024-04-17 10:15:56 +0400219 env, err := appManager.Config()
gio3af43942024-04-16 08:13:50 +0400220 if err != nil {
221 http.Error(w, err.Error(), http.StatusInternalServerError)
222 return
223 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400224 appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
225 {
gio3cdee592024-04-17 10:15:56 +0400226 app, err := installer.FindEnvApp(appsRepo, "headscale-user")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400227 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400228 http.Error(w, err.Error(), http.StatusInternalServerError)
229 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400230 }
gio44f621b2024-04-29 09:44:38 +0400231 instanceId := fmt.Sprintf("%s-%s", app.Slug(), req.Username)
gio3cdee592024-04-17 10:15:56 +0400232 appDir := fmt.Sprintf("/apps/%s", instanceId)
233 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
gio778577f2024-04-29 09:44:38 +0400234 if _, err := appManager.Install(app, instanceId, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400235 "username": req.Username,
236 "preAuthKey": map[string]any{
237 "enabled": false,
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400238 },
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400239 }); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400240 http.Error(w, err.Error(), http.StatusInternalServerError)
241 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400242 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400243 }
244 }
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400245 renderRegistrationSuccess(w, s.loginAddr)
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400246}
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400247
gio2728e402024-08-01 18:14:21 +0400248type firstAccount struct {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400249 Created bool `json:"created"`
gio2728e402024-08-01 18:14:21 +0400250 Domain string `json:"domain"`
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400251 Groups []string `json:"groups"`
252}
253
254type initRequest struct {
gio2728e402024-08-01 18:14:21 +0400255 User string `json:"user"`
256 Email string `json:"email"`
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400257 Groups []string `json:"groups"`
258}
259
gio2728e402024-08-01 18:14:21 +0400260type createUserRequest struct {
261 User string `json:"user"`
262 Email string `json:"email"`
263}
264
265func (s *Server) createUser(username string) error {
gioe72b54f2024-04-22 10:44:41 +0400266 return s.repo.Do(func(r soft.RepoFS) (string, error) {
gio2728e402024-08-01 18:14:21 +0400267 var fa firstAccount
gioe72b54f2024-04-22 10:44:41 +0400268 if err := soft.ReadYaml(r, "first-account.yaml", &fa); err != nil {
gio3af43942024-04-16 08:13:50 +0400269 return "", err
270 }
gio2728e402024-08-01 18:14:21 +0400271 var resp *http.Response
272 var err error
gio3af43942024-04-16 08:13:50 +0400273 if fa.Created {
gio2728e402024-08-01 18:14:21 +0400274 req := createUserRequest{username, fmt.Sprintf("%s@%s", username, fa.Domain)}
275 var buf bytes.Buffer
276 if err := json.NewEncoder(&buf).Encode(req); err != nil {
277 return "", err
278 }
279 resp, err = http.Post(
280 fmt.Sprintf("%s/api/users", s.membershipsAddr),
281 "applications/json",
282 &buf,
283 )
284 } else {
285 req := initRequest{username, fmt.Sprintf("%s@%s", username, fa.Domain), fa.Groups}
286 var buf bytes.Buffer
287 if err := json.NewEncoder(&buf).Encode(req); err != nil {
288 return "", err
289 }
290 resp, err = http.Post(
291 fmt.Sprintf("%s/api/init", s.membershipsAddr),
292 "applications/json",
293 &buf,
294 )
295 fa.Created = true
296 if err := soft.WriteYaml(r, "first-account.yaml", fa); err != nil {
297 return "", err
298 }
gio3af43942024-04-16 08:13:50 +0400299 }
gio4784f8e2024-08-01 15:20:12 +0400300 if err != nil {
gio3af43942024-04-16 08:13:50 +0400301 return "", err
302 }
gio4784f8e2024-08-01 15:20:12 +0400303 defer resp.Body.Close()
304 fmt.Printf("Memberships resp: %d", resp.StatusCode)
305 io.Copy(os.Stdout, resp.Body)
gio2728e402024-08-01 18:14:21 +0400306 if resp.StatusCode != http.StatusOK {
307 return "", fmt.Errorf("memberships error")
gio3af43942024-04-16 08:13:50 +0400308 }
309 return "initialized groups for first account", nil
310 })
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400311}