blob: 64f4cf1eb2b985fd9d30598afcc8c3fd0aa67f03 [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
33 createAccountAddr string
34 loginAddr string
35 membershipsInitAddr string
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040036}
37
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040038func NewServer(
39 port int,
gioe72b54f2024-04-22 10:44:41 +040040 repo soft.RepoIO,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040041 nsCreator installer.NamespaceCreator,
42 createAccountAddr string,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040043 loginAddr string,
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040044 membershipsInitAddr string,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040045) *Server {
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040046 return &Server{
47 port,
48 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040049 nsCreator,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040050 createAccountAddr,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040051 loginAddr,
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040052 membershipsInitAddr,
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040053 }
54}
55
56func (s *Server) Start() {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040057 r := mux.NewRouter()
gio09f8efa2024-06-10 22:35:24 +040058 r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040059 r.Path("/").Methods("POST").HandlerFunc(s.createAccount)
60 r.Path("/").Methods("GET").HandlerFunc(s.createAccountForm)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040061 http.Handle("/", r)
62 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040063}
64
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040065func (s *Server) createAccountForm(w http.ResponseWriter, r *http.Request) {
DTabidze52593392024-03-08 12:53:20 +040066 renderRegistrationForm(w, formData{})
67}
68
69type formData struct {
70 UsernameErrors []string
71 PasswordErrors []string
72 Data createAccountReq
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040073}
74
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040075type createAccountReq struct {
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040076 Username string `json:"username,omitempty"`
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040077 Password string `json:"password,omitempty"`
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040078 SecretToken string `json:"secretToken,omitempty"`
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040079}
80
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040081type apiCreateAccountReq struct {
82 Username string `json:"username,omitempty"`
83 Password string `json:"password,omitempty"`
84}
85
DTabidze52593392024-03-08 12:53:20 +040086type ValidationError struct {
87 Field string `json:"field"`
88 Message string `json:"message"`
89}
90
91type ErrorResponse struct {
92 Errors []ValidationError `json:"errors"`
93}
94
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +040095func getFormValue(v url.Values, name string) (string, error) {
96 items, ok := v[name]
97 if !ok || len(items) != 1 {
98 return "", fmt.Errorf("%s not found", name)
99 }
100 return items[0], nil
101}
102
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400103func extractReq(r *http.Request) (createAccountReq, error) {
104 var req createAccountReq
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400105 if err := func() error {
106 var err error
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400107 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400108 return err
109 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400110 if req.Username, err = getFormValue(r.PostForm, "username"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400111 return err
112 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400113 if req.Password, err = getFormValue(r.PostForm, "password"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400114 return err
115 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400116 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400117 return err
118 }
119 return nil
120 }(); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400121 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
122 return createAccountReq{}, err
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400123 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400124 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400125 return req, nil
126}
127
DTabidze52593392024-03-08 12:53:20 +0400128func renderRegistrationForm(w http.ResponseWriter, data formData) {
129 tmpl, err := template.New("create-account").Parse(string(indexHtml))
130 if err != nil {
131 http.Error(w, err.Error(), http.StatusInternalServerError)
132 return
133 }
134 if err := tmpl.Execute(w, data); err != nil {
135 http.Error(w, err.Error(), http.StatusInternalServerError)
136 return
137 }
138}
139
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400140func renderRegistrationSuccess(w http.ResponseWriter, loginAddr string) {
141 data := struct {
142 LoginAddr string
143 }{
144 LoginAddr: loginAddr,
145 }
146 tmpl, err := template.New("create-account-success").Parse(string(successHtml))
147 if err != nil {
148 http.Error(w, err.Error(), http.StatusInternalServerError)
149 return
150 }
151 if err := tmpl.Execute(w, data); err != nil {
152 http.Error(w, err.Error(), http.StatusInternalServerError)
153 return
154 }
155}
156
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400157func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400158 req, err := extractReq(r)
159 if err != nil {
160 http.Error(w, err.Error(), http.StatusInternalServerError)
161 return
162 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400163 {
164 var buf bytes.Buffer
165 cr := apiCreateAccountReq{req.Username, req.Password}
166 if err := json.NewEncoder(&buf).Encode(cr); err != nil {
167 http.Error(w, err.Error(), http.StatusInternalServerError)
168 return
169 }
170 resp, err := http.Post(s.createAccountAddr, "application/json", &buf)
171 if err != nil {
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400172 var respBody bytes.Buffer
173 if _, err := io.Copy(&respBody, resp.Body); err != nil {
174 http.Error(w, err.Error(), http.StatusInternalServerError)
175 }
176 respStr := respBody.String()
177 log.Println(respStr)
178 http.Error(w, respStr, http.StatusInternalServerError)
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400179 return
180 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400181 if resp.StatusCode != http.StatusOK {
DTabidze52593392024-03-08 12:53:20 +0400182 var errResponse ErrorResponse
183 if err := json.NewDecoder(resp.Body).Decode(&errResponse); err != nil {
184 http.Error(w, "Error Decoding JSON", http.StatusInternalServerError)
185 return
186 }
187 var usernameErrors, passwordErrors []string
188 for _, err := range errResponse.Errors {
189 if err.Field == "username" {
190 usernameErrors = append(usernameErrors, err.Message)
191 }
192 if err.Field == "password" {
193 passwordErrors = append(passwordErrors, err.Message)
194 }
195 }
196 renderRegistrationForm(w, formData{
197 usernameErrors,
198 passwordErrors,
199 req,
200 })
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400201 return
202 }
203 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400204 if err := s.initMemberships(req.Username); err != nil {
205 http.Error(w, err.Error(), http.StatusInternalServerError)
206 return
207 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400208 {
gio308105e2024-04-19 13:12:13 +0400209 appManager, err := installer.NewAppManager(s.repo, s.nsCreator, "/apps")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400210 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400211 http.Error(w, err.Error(), http.StatusInternalServerError)
212 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400213 }
gio3cdee592024-04-17 10:15:56 +0400214 env, err := appManager.Config()
gio3af43942024-04-16 08:13:50 +0400215 if err != nil {
216 http.Error(w, err.Error(), http.StatusInternalServerError)
217 return
218 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400219 appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
220 {
gio3cdee592024-04-17 10:15:56 +0400221 app, err := installer.FindEnvApp(appsRepo, "headscale-user")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400222 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400223 http.Error(w, err.Error(), http.StatusInternalServerError)
224 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400225 }
gio44f621b2024-04-29 09:44:38 +0400226 instanceId := fmt.Sprintf("%s-%s", app.Slug(), req.Username)
gio3cdee592024-04-17 10:15:56 +0400227 appDir := fmt.Sprintf("/apps/%s", instanceId)
228 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
gio778577f2024-04-29 09:44:38 +0400229 if _, err := appManager.Install(app, instanceId, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400230 "username": req.Username,
231 "preAuthKey": map[string]any{
232 "enabled": false,
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400233 },
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400234 }); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400235 http.Error(w, err.Error(), http.StatusInternalServerError)
236 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400237 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400238 }
239 }
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400240 renderRegistrationSuccess(w, s.loginAddr)
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400241}
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400242
243type firstaccount struct {
244 Created bool `json:"created"`
245 Groups []string `json:"groups"`
246}
247
248type initRequest struct {
249 Owner string `json:"owner"`
250 Groups []string `json:"groups"`
251}
252
253func (s *Server) initMemberships(username string) error {
gioe72b54f2024-04-22 10:44:41 +0400254 return s.repo.Do(func(r soft.RepoFS) (string, error) {
gio3af43942024-04-16 08:13:50 +0400255 var fa firstaccount
gioe72b54f2024-04-22 10:44:41 +0400256 if err := soft.ReadYaml(r, "first-account.yaml", &fa); err != nil {
gio3af43942024-04-16 08:13:50 +0400257 return "", err
258 }
259 if fa.Created {
260 return "", nil
261 }
262 req := initRequest{username, fa.Groups}
263 var buf bytes.Buffer
264 if err := json.NewEncoder(&buf).Encode(req); err != nil {
265 return "", err
266 }
267 if _, err := http.Post(s.membershipsInitAddr, "applications/json", &buf); err != nil {
268 return "", err
269 }
270 fa.Created = true
gioe72b54f2024-04-22 10:44:41 +0400271 if err := soft.WriteYaml(r, "first-account.yaml", fa); err != nil {
gio3af43942024-04-16 08:13:50 +0400272 return "", err
273 }
274 return "initialized groups for first account", nil
275 })
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400276}