blob: 9e45de59255124346c28dc78b1daed0ac5c5c495 [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
gio1bf00802024-08-17 12:31:41 +040030//go:embed stat/*
31var statAssets embed.FS
32
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040033type Server struct {
gio2728e402024-08-01 18:14:21 +040034 port int
35 repo soft.RepoIO
36 nsCreator installer.NamespaceCreator
37 hf installer.HelmFetcher
38 createAccountAddr string
39 loginAddr string
40 membershipsAddr string
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040041}
42
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040043func NewServer(
44 port int,
gioe72b54f2024-04-22 10:44:41 +040045 repo soft.RepoIO,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040046 nsCreator installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +040047 hf installer.HelmFetcher,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040048 createAccountAddr string,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040049 loginAddr string,
gio2728e402024-08-01 18:14:21 +040050 membershipsAddr string,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040051) *Server {
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040052 return &Server{
53 port,
54 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040055 nsCreator,
giof8843412024-05-22 16:38:05 +040056 hf,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040057 createAccountAddr,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040058 loginAddr,
gio2728e402024-08-01 18:14:21 +040059 membershipsAddr,
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040060 }
61}
62
63func (s *Server) Start() {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040064 r := mux.NewRouter()
gio1bf00802024-08-17 12:31:41 +040065 r.PathPrefix("/stat/").Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040066 r.Path("/").Methods("POST").HandlerFunc(s.createAccount)
67 r.Path("/").Methods("GET").HandlerFunc(s.createAccountForm)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040068 http.Handle("/", r)
69 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040070}
71
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040072func (s *Server) createAccountForm(w http.ResponseWriter, r *http.Request) {
DTabidze52593392024-03-08 12:53:20 +040073 renderRegistrationForm(w, formData{})
74}
75
76type formData struct {
77 UsernameErrors []string
78 PasswordErrors []string
79 Data createAccountReq
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040080}
81
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040082type createAccountReq struct {
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040083 Username string `json:"username,omitempty"`
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040084 Password string `json:"password,omitempty"`
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040085 SecretToken string `json:"secretToken,omitempty"`
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040086}
87
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040088type apiCreateAccountReq struct {
89 Username string `json:"username,omitempty"`
90 Password string `json:"password,omitempty"`
91}
92
DTabidze52593392024-03-08 12:53:20 +040093type ValidationError struct {
94 Field string `json:"field"`
95 Message string `json:"message"`
96}
97
98type ErrorResponse struct {
99 Errors []ValidationError `json:"errors"`
100}
101
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400102func getFormValue(v url.Values, name string) (string, error) {
103 items, ok := v[name]
104 if !ok || len(items) != 1 {
105 return "", fmt.Errorf("%s not found", name)
106 }
107 return items[0], nil
108}
109
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400110func extractReq(r *http.Request) (createAccountReq, error) {
111 var req createAccountReq
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400112 if err := func() error {
113 var err error
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400114 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400115 return err
116 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400117 if req.Username, err = getFormValue(r.PostForm, "username"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400118 return err
119 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400120 if req.Password, err = getFormValue(r.PostForm, "password"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400121 return err
122 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400123 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400124 return err
125 }
126 return nil
127 }(); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400128 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
129 return createAccountReq{}, err
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400130 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400131 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400132 return req, nil
133}
134
DTabidze52593392024-03-08 12:53:20 +0400135func renderRegistrationForm(w http.ResponseWriter, data formData) {
136 tmpl, err := template.New("create-account").Parse(string(indexHtml))
137 if err != nil {
138 http.Error(w, err.Error(), http.StatusInternalServerError)
139 return
140 }
141 if err := tmpl.Execute(w, data); err != nil {
142 http.Error(w, err.Error(), http.StatusInternalServerError)
143 return
144 }
145}
146
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400147func renderRegistrationSuccess(w http.ResponseWriter, loginAddr string) {
148 data := struct {
149 LoginAddr string
150 }{
151 LoginAddr: loginAddr,
152 }
153 tmpl, err := template.New("create-account-success").Parse(string(successHtml))
154 if err != nil {
155 http.Error(w, err.Error(), http.StatusInternalServerError)
156 return
157 }
158 if err := tmpl.Execute(w, data); err != nil {
159 http.Error(w, err.Error(), http.StatusInternalServerError)
160 return
161 }
162}
163
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400164func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400165 req, err := extractReq(r)
166 if err != nil {
167 http.Error(w, err.Error(), http.StatusInternalServerError)
168 return
169 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400170 {
171 var buf bytes.Buffer
172 cr := apiCreateAccountReq{req.Username, req.Password}
173 if err := json.NewEncoder(&buf).Encode(cr); err != nil {
174 http.Error(w, err.Error(), http.StatusInternalServerError)
175 return
176 }
177 resp, err := http.Post(s.createAccountAddr, "application/json", &buf)
178 if err != nil {
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400179 var respBody bytes.Buffer
180 if _, err := io.Copy(&respBody, resp.Body); err != nil {
181 http.Error(w, err.Error(), http.StatusInternalServerError)
182 }
183 respStr := respBody.String()
184 log.Println(respStr)
185 http.Error(w, respStr, http.StatusInternalServerError)
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400186 return
187 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400188 if resp.StatusCode != http.StatusOK {
DTabidze52593392024-03-08 12:53:20 +0400189 var errResponse ErrorResponse
190 if err := json.NewDecoder(resp.Body).Decode(&errResponse); err != nil {
191 http.Error(w, "Error Decoding JSON", http.StatusInternalServerError)
192 return
193 }
194 var usernameErrors, passwordErrors []string
195 for _, err := range errResponse.Errors {
196 if err.Field == "username" {
197 usernameErrors = append(usernameErrors, err.Message)
198 }
199 if err.Field == "password" {
200 passwordErrors = append(passwordErrors, err.Message)
201 }
202 }
203 renderRegistrationForm(w, formData{
204 usernameErrors,
205 passwordErrors,
206 req,
207 })
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400208 return
209 }
210 }
gio2728e402024-08-01 18:14:21 +0400211 if err := s.createUser(req.Username); err != nil {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400212 http.Error(w, err.Error(), http.StatusInternalServerError)
213 return
214 }
giof8843412024-05-22 16:38:05 +0400215 // TODO(gio): remove this once auto user sync is implemented
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400216 {
giof8843412024-05-22 16:38:05 +0400217 appManager, err := installer.NewAppManager(s.repo, s.nsCreator, nil, s.hf, "/apps")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400218 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400219 http.Error(w, err.Error(), http.StatusInternalServerError)
220 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400221 }
gio3cdee592024-04-17 10:15:56 +0400222 env, err := appManager.Config()
gio3af43942024-04-16 08:13:50 +0400223 if err != nil {
224 http.Error(w, err.Error(), http.StatusInternalServerError)
225 return
226 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400227 appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
228 {
gio3cdee592024-04-17 10:15:56 +0400229 app, err := installer.FindEnvApp(appsRepo, "headscale-user")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400230 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400231 http.Error(w, err.Error(), http.StatusInternalServerError)
232 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400233 }
gio44f621b2024-04-29 09:44:38 +0400234 instanceId := fmt.Sprintf("%s-%s", app.Slug(), req.Username)
gio3cdee592024-04-17 10:15:56 +0400235 appDir := fmt.Sprintf("/apps/%s", instanceId)
236 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
gio778577f2024-04-29 09:44:38 +0400237 if _, err := appManager.Install(app, instanceId, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400238 "username": req.Username,
239 "preAuthKey": map[string]any{
240 "enabled": false,
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400241 },
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400242 }); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400243 http.Error(w, err.Error(), http.StatusInternalServerError)
244 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400245 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400246 }
247 }
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400248 renderRegistrationSuccess(w, s.loginAddr)
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400249}
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400250
gio2728e402024-08-01 18:14:21 +0400251type firstAccount struct {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400252 Created bool `json:"created"`
gio2728e402024-08-01 18:14:21 +0400253 Domain string `json:"domain"`
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400254 Groups []string `json:"groups"`
255}
256
257type initRequest struct {
gio2728e402024-08-01 18:14:21 +0400258 User string `json:"user"`
259 Email string `json:"email"`
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400260 Groups []string `json:"groups"`
261}
262
gio2728e402024-08-01 18:14:21 +0400263type createUserRequest struct {
264 User string `json:"user"`
265 Email string `json:"email"`
266}
267
268func (s *Server) createUser(username string) error {
giob4a3a192024-08-19 09:55:47 +0400269 _, err := s.repo.Do(func(r soft.RepoFS) (string, error) {
gio2728e402024-08-01 18:14:21 +0400270 var fa firstAccount
gioe72b54f2024-04-22 10:44:41 +0400271 if err := soft.ReadYaml(r, "first-account.yaml", &fa); err != nil {
gio3af43942024-04-16 08:13:50 +0400272 return "", err
273 }
gio2728e402024-08-01 18:14:21 +0400274 var resp *http.Response
275 var err error
gio3af43942024-04-16 08:13:50 +0400276 if fa.Created {
gio2728e402024-08-01 18:14:21 +0400277 req := createUserRequest{username, fmt.Sprintf("%s@%s", username, fa.Domain)}
278 var buf bytes.Buffer
279 if err := json.NewEncoder(&buf).Encode(req); err != nil {
280 return "", err
281 }
282 resp, err = http.Post(
283 fmt.Sprintf("%s/api/users", s.membershipsAddr),
284 "applications/json",
285 &buf,
286 )
287 } else {
288 req := initRequest{username, fmt.Sprintf("%s@%s", username, fa.Domain), fa.Groups}
289 var buf bytes.Buffer
290 if err := json.NewEncoder(&buf).Encode(req); err != nil {
291 return "", err
292 }
293 resp, err = http.Post(
294 fmt.Sprintf("%s/api/init", s.membershipsAddr),
295 "applications/json",
296 &buf,
297 )
298 fa.Created = true
299 if err := soft.WriteYaml(r, "first-account.yaml", fa); err != nil {
300 return "", err
301 }
gio3af43942024-04-16 08:13:50 +0400302 }
gio4784f8e2024-08-01 15:20:12 +0400303 if err != nil {
gio3af43942024-04-16 08:13:50 +0400304 return "", err
305 }
gio4784f8e2024-08-01 15:20:12 +0400306 defer resp.Body.Close()
307 fmt.Printf("Memberships resp: %d", resp.StatusCode)
308 io.Copy(os.Stdout, resp.Body)
gio2728e402024-08-01 18:14:21 +0400309 if resp.StatusCode != http.StatusOK {
310 return "", fmt.Errorf("memberships error")
gio3af43942024-04-16 08:13:50 +0400311 }
312 return "initialized groups for first account", nil
313 })
giob4a3a192024-08-19 09:55:47 +0400314 return err
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400315}