blob: 7688d5044ae9cb46c613b19d51ed7397987b1866 [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"
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040013
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040014 "github.com/gorilla/mux"
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040015
16 "github.com/giolekva/pcloud/core/installer"
gioe72b54f2024-04-22 10:44:41 +040017 "github.com/giolekva/pcloud/core/installer/soft"
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040018)
19
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040020//go:embed create-account.html
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040021var indexHtml []byte
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040022
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040023//go:embed create-account-success.html
24var successHtml []byte
25
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040026//go:embed static/*
27var staticAssets embed.FS
28
29type Server struct {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040030 port int
gioe72b54f2024-04-22 10:44:41 +040031 repo soft.RepoIO
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040032 nsCreator installer.NamespaceCreator
giof8843412024-05-22 16:38:05 +040033 hf installer.HelmFetcher
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040034 createAccountAddr string
35 loginAddr string
36 membershipsInitAddr string
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040037}
38
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040039func NewServer(
40 port int,
gioe72b54f2024-04-22 10:44:41 +040041 repo soft.RepoIO,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040042 nsCreator installer.NamespaceCreator,
giof8843412024-05-22 16:38:05 +040043 hf installer.HelmFetcher,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040044 createAccountAddr string,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040045 loginAddr string,
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040046 membershipsInitAddr string,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040047) *Server {
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040048 return &Server{
49 port,
50 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040051 nsCreator,
giof8843412024-05-22 16:38:05 +040052 hf,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040053 createAccountAddr,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040054 loginAddr,
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040055 membershipsInitAddr,
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040056 }
57}
58
59func (s *Server) Start() {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040060 r := mux.NewRouter()
gio09f8efa2024-06-10 22:35:24 +040061 r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040062 r.Path("/").Methods("POST").HandlerFunc(s.createAccount)
63 r.Path("/").Methods("GET").HandlerFunc(s.createAccountForm)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040064 http.Handle("/", r)
65 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040066}
67
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040068func (s *Server) createAccountForm(w http.ResponseWriter, r *http.Request) {
DTabidze52593392024-03-08 12:53:20 +040069 renderRegistrationForm(w, formData{})
70}
71
72type formData struct {
73 UsernameErrors []string
74 PasswordErrors []string
75 Data createAccountReq
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040076}
77
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040078type createAccountReq struct {
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040079 Username string `json:"username,omitempty"`
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040080 Password string `json:"password,omitempty"`
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040081 SecretToken string `json:"secretToken,omitempty"`
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040082}
83
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040084type apiCreateAccountReq struct {
85 Username string `json:"username,omitempty"`
86 Password string `json:"password,omitempty"`
87}
88
DTabidze52593392024-03-08 12:53:20 +040089type ValidationError struct {
90 Field string `json:"field"`
91 Message string `json:"message"`
92}
93
94type ErrorResponse struct {
95 Errors []ValidationError `json:"errors"`
96}
97
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +040098func getFormValue(v url.Values, name string) (string, error) {
99 items, ok := v[name]
100 if !ok || len(items) != 1 {
101 return "", fmt.Errorf("%s not found", name)
102 }
103 return items[0], nil
104}
105
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400106func extractReq(r *http.Request) (createAccountReq, error) {
107 var req createAccountReq
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400108 if err := func() error {
109 var err error
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400110 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400111 return err
112 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400113 if req.Username, err = getFormValue(r.PostForm, "username"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400114 return err
115 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400116 if req.Password, err = getFormValue(r.PostForm, "password"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400117 return err
118 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400119 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400120 return err
121 }
122 return nil
123 }(); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400124 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
125 return createAccountReq{}, err
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400126 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400127 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400128 return req, nil
129}
130
DTabidze52593392024-03-08 12:53:20 +0400131func renderRegistrationForm(w http.ResponseWriter, data formData) {
132 tmpl, err := template.New("create-account").Parse(string(indexHtml))
133 if err != nil {
134 http.Error(w, err.Error(), http.StatusInternalServerError)
135 return
136 }
137 if err := tmpl.Execute(w, data); err != nil {
138 http.Error(w, err.Error(), http.StatusInternalServerError)
139 return
140 }
141}
142
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400143func renderRegistrationSuccess(w http.ResponseWriter, loginAddr string) {
144 data := struct {
145 LoginAddr string
146 }{
147 LoginAddr: loginAddr,
148 }
149 tmpl, err := template.New("create-account-success").Parse(string(successHtml))
150 if err != nil {
151 http.Error(w, err.Error(), http.StatusInternalServerError)
152 return
153 }
154 if err := tmpl.Execute(w, data); err != nil {
155 http.Error(w, err.Error(), http.StatusInternalServerError)
156 return
157 }
158}
159
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400160func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400161 req, err := extractReq(r)
162 if err != nil {
163 http.Error(w, err.Error(), http.StatusInternalServerError)
164 return
165 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400166 {
167 var buf bytes.Buffer
168 cr := apiCreateAccountReq{req.Username, req.Password}
169 if err := json.NewEncoder(&buf).Encode(cr); err != nil {
170 http.Error(w, err.Error(), http.StatusInternalServerError)
171 return
172 }
173 resp, err := http.Post(s.createAccountAddr, "application/json", &buf)
174 if err != nil {
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400175 var respBody bytes.Buffer
176 if _, err := io.Copy(&respBody, resp.Body); err != nil {
177 http.Error(w, err.Error(), http.StatusInternalServerError)
178 }
179 respStr := respBody.String()
180 log.Println(respStr)
181 http.Error(w, respStr, http.StatusInternalServerError)
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400182 return
183 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400184 if resp.StatusCode != http.StatusOK {
DTabidze52593392024-03-08 12:53:20 +0400185 var errResponse ErrorResponse
186 if err := json.NewDecoder(resp.Body).Decode(&errResponse); err != nil {
187 http.Error(w, "Error Decoding JSON", http.StatusInternalServerError)
188 return
189 }
190 var usernameErrors, passwordErrors []string
191 for _, err := range errResponse.Errors {
192 if err.Field == "username" {
193 usernameErrors = append(usernameErrors, err.Message)
194 }
195 if err.Field == "password" {
196 passwordErrors = append(passwordErrors, err.Message)
197 }
198 }
199 renderRegistrationForm(w, formData{
200 usernameErrors,
201 passwordErrors,
202 req,
203 })
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400204 return
205 }
206 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400207 if err := s.initMemberships(req.Username); err != nil {
208 http.Error(w, err.Error(), http.StatusInternalServerError)
209 return
210 }
giof8843412024-05-22 16:38:05 +0400211 // TODO(gio): remove this once auto user sync is implemented
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400212 {
giof8843412024-05-22 16:38:05 +0400213 appManager, err := installer.NewAppManager(s.repo, s.nsCreator, nil, s.hf, "/apps")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400214 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400215 http.Error(w, err.Error(), http.StatusInternalServerError)
216 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400217 }
gio3cdee592024-04-17 10:15:56 +0400218 env, err := appManager.Config()
gio3af43942024-04-16 08:13:50 +0400219 if err != nil {
220 http.Error(w, err.Error(), http.StatusInternalServerError)
221 return
222 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400223 appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
224 {
gio3cdee592024-04-17 10:15:56 +0400225 app, err := installer.FindEnvApp(appsRepo, "headscale-user")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400226 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400227 http.Error(w, err.Error(), http.StatusInternalServerError)
228 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400229 }
gio44f621b2024-04-29 09:44:38 +0400230 instanceId := fmt.Sprintf("%s-%s", app.Slug(), req.Username)
gio3cdee592024-04-17 10:15:56 +0400231 appDir := fmt.Sprintf("/apps/%s", instanceId)
232 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
gio778577f2024-04-29 09:44:38 +0400233 if _, err := appManager.Install(app, instanceId, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400234 "username": req.Username,
235 "preAuthKey": map[string]any{
236 "enabled": false,
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400237 },
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400238 }); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400239 http.Error(w, err.Error(), http.StatusInternalServerError)
240 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400241 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400242 }
243 }
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400244 renderRegistrationSuccess(w, s.loginAddr)
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400245}
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400246
247type firstaccount struct {
248 Created bool `json:"created"`
249 Groups []string `json:"groups"`
250}
251
252type initRequest struct {
253 Owner string `json:"owner"`
254 Groups []string `json:"groups"`
255}
256
257func (s *Server) initMemberships(username string) error {
gioe72b54f2024-04-22 10:44:41 +0400258 return s.repo.Do(func(r soft.RepoFS) (string, error) {
gio3af43942024-04-16 08:13:50 +0400259 var fa firstaccount
gioe72b54f2024-04-22 10:44:41 +0400260 if err := soft.ReadYaml(r, "first-account.yaml", &fa); err != nil {
gio3af43942024-04-16 08:13:50 +0400261 return "", err
262 }
263 if fa.Created {
264 return "", nil
265 }
266 req := initRequest{username, fa.Groups}
267 var buf bytes.Buffer
268 if err := json.NewEncoder(&buf).Encode(req); err != nil {
269 return "", err
270 }
271 if _, err := http.Post(s.membershipsInitAddr, "applications/json", &buf); err != nil {
272 return "", err
273 }
274 fa.Created = true
gioe72b54f2024-04-22 10:44:41 +0400275 if err := soft.WriteYaml(r, "first-account.yaml", fa); err != nil {
gio3af43942024-04-16 08:13:50 +0400276 return "", err
277 }
278 return "initialized groups for first account", nil
279 })
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400280}