blob: fe6c560850b83274b1176b0db3ac55a9ab927878 [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"
17)
18
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040019//go:embed create-account.html
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040020var indexHtml []byte
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040021
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040022//go:embed create-account-success.html
23var successHtml []byte
24
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040025//go:embed static/*
26var staticAssets embed.FS
27
28type Server struct {
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040029 port int
30 repo installer.RepoIO
31 nsCreator installer.NamespaceCreator
32 createAccountAddr string
33 loginAddr string
34 membershipsInitAddr string
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040035}
36
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040037func NewServer(
38 port int,
39 repo installer.RepoIO,
40 nsCreator installer.NamespaceCreator,
41 createAccountAddr string,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040042 loginAddr string,
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040043 membershipsInitAddr string,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040044) *Server {
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040045 return &Server{
46 port,
47 repo,
Giorgi Lekveishvili7fb28bf2023-06-24 19:51:16 +040048 nsCreator,
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040049 createAccountAddr,
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +040050 loginAddr,
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040051 membershipsInitAddr,
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040052 }
53}
54
55func (s *Server) Start() {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040056 r := mux.NewRouter()
57 r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040058 r.Path("/").Methods("POST").HandlerFunc(s.createAccount)
59 r.Path("/").Methods("GET").HandlerFunc(s.createAccountForm)
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040060 http.Handle("/", r)
61 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040062}
63
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +040064func (s *Server) createAccountForm(w http.ResponseWriter, r *http.Request) {
DTabidze52593392024-03-08 12:53:20 +040065 renderRegistrationForm(w, formData{})
66}
67
68type formData struct {
69 UsernameErrors []string
70 PasswordErrors []string
71 Data createAccountReq
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040072}
73
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +040074type createAccountReq struct {
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040075 Username string `json:"username,omitempty"`
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040076 Password string `json:"password,omitempty"`
Giorgi Lekveishvili260a97d2023-12-08 15:04:16 +040077 SecretToken string `json:"secretToken,omitempty"`
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +040078}
79
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +040080type apiCreateAccountReq struct {
81 Username string `json:"username,omitempty"`
82 Password string `json:"password,omitempty"`
83}
84
DTabidze52593392024-03-08 12:53:20 +040085type ValidationError struct {
86 Field string `json:"field"`
87 Message string `json:"message"`
88}
89
90type ErrorResponse struct {
91 Errors []ValidationError `json:"errors"`
92}
93
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +040094func getFormValue(v url.Values, name string) (string, error) {
95 items, ok := v[name]
96 if !ok || len(items) != 1 {
97 return "", fmt.Errorf("%s not found", name)
98 }
99 return items[0], nil
100}
101
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400102func extractReq(r *http.Request) (createAccountReq, error) {
103 var req createAccountReq
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400104 if err := func() error {
105 var err error
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400106 if err = r.ParseForm(); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400107 return err
108 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400109 if req.Username, err = getFormValue(r.PostForm, "username"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400110 return err
111 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400112 if req.Password, err = getFormValue(r.PostForm, "password"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400113 return err
114 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400115 if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400116 return err
117 }
118 return nil
119 }(); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400120 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
121 return createAccountReq{}, err
Giorgi Lekveishvili6b887be2023-06-22 14:38:19 +0400122 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400123 }
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400124 return req, nil
125}
126
DTabidze52593392024-03-08 12:53:20 +0400127func renderRegistrationForm(w http.ResponseWriter, data formData) {
128 tmpl, err := template.New("create-account").Parse(string(indexHtml))
129 if err != nil {
130 http.Error(w, err.Error(), http.StatusInternalServerError)
131 return
132 }
133 if err := tmpl.Execute(w, data); err != nil {
134 http.Error(w, err.Error(), http.StatusInternalServerError)
135 return
136 }
137}
138
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400139func renderRegistrationSuccess(w http.ResponseWriter, loginAddr string) {
140 data := struct {
141 LoginAddr string
142 }{
143 LoginAddr: loginAddr,
144 }
145 tmpl, err := template.New("create-account-success").Parse(string(successHtml))
146 if err != nil {
147 http.Error(w, err.Error(), http.StatusInternalServerError)
148 return
149 }
150 if err := tmpl.Execute(w, data); err != nil {
151 http.Error(w, err.Error(), http.StatusInternalServerError)
152 return
153 }
154}
155
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400156func (s *Server) createAccount(w http.ResponseWriter, r *http.Request) {
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400157 req, err := extractReq(r)
158 if err != nil {
159 http.Error(w, err.Error(), http.StatusInternalServerError)
160 return
161 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400162 {
163 var buf bytes.Buffer
164 cr := apiCreateAccountReq{req.Username, req.Password}
165 if err := json.NewEncoder(&buf).Encode(cr); err != nil {
166 http.Error(w, err.Error(), http.StatusInternalServerError)
167 return
168 }
169 resp, err := http.Post(s.createAccountAddr, "application/json", &buf)
170 if err != nil {
Giorgi Lekveishvili83399052024-02-14 13:27:30 +0400171 var respBody bytes.Buffer
172 if _, err := io.Copy(&respBody, resp.Body); err != nil {
173 http.Error(w, err.Error(), http.StatusInternalServerError)
174 }
175 respStr := respBody.String()
176 log.Println(respStr)
177 http.Error(w, respStr, http.StatusInternalServerError)
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400178 return
179 }
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400180 if resp.StatusCode != http.StatusOK {
DTabidze52593392024-03-08 12:53:20 +0400181 var errResponse ErrorResponse
182 if err := json.NewDecoder(resp.Body).Decode(&errResponse); err != nil {
183 http.Error(w, "Error Decoding JSON", http.StatusInternalServerError)
184 return
185 }
186 var usernameErrors, passwordErrors []string
187 for _, err := range errResponse.Errors {
188 if err.Field == "username" {
189 usernameErrors = append(usernameErrors, err.Message)
190 }
191 if err.Field == "password" {
192 passwordErrors = append(passwordErrors, err.Message)
193 }
194 }
195 renderRegistrationForm(w, formData{
196 usernameErrors,
197 passwordErrors,
198 req,
199 })
Giorgi Lekveishvilic89b9002023-12-21 13:09:26 +0400200 return
201 }
202 }
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400203 if err := s.initMemberships(req.Username); err != nil {
204 http.Error(w, err.Error(), http.StatusInternalServerError)
205 return
206 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400207 {
gio308105e2024-04-19 13:12:13 +0400208 appManager, err := installer.NewAppManager(s.repo, s.nsCreator, "/apps")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400209 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400210 http.Error(w, err.Error(), http.StatusInternalServerError)
211 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400212 }
gio3cdee592024-04-17 10:15:56 +0400213 env, err := appManager.Config()
gio3af43942024-04-16 08:13:50 +0400214 if err != nil {
215 http.Error(w, err.Error(), http.StatusInternalServerError)
216 return
217 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400218 appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
219 {
gio3cdee592024-04-17 10:15:56 +0400220 app, err := installer.FindEnvApp(appsRepo, "headscale-user")
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400221 if err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400222 http.Error(w, err.Error(), http.StatusInternalServerError)
223 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400224 }
gio3cdee592024-04-17 10:15:56 +0400225 instanceId := fmt.Sprintf("%s-%s", app.Name(), req.Username)
226 appDir := fmt.Sprintf("/apps/%s", instanceId)
227 namespace := fmt.Sprintf("%s%s", env.NamespacePrefix, app.Namespace())
228 if err := appManager.Install(app, instanceId, appDir, namespace, map[string]any{
Giorgi Lekveishvilie009a5d2024-01-05 14:10:11 +0400229 "username": req.Username,
230 "preAuthKey": map[string]any{
231 "enabled": false,
Giorgi Lekveishvili39913692023-12-05 08:58:08 +0400232 },
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400233 }); err != nil {
Giorgi Lekveishvili123a3672023-12-04 13:01:29 +0400234 http.Error(w, err.Error(), http.StatusInternalServerError)
235 return
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400236 }
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400237 }
238 }
Giorgi Lekveishvili83b72192024-03-11 18:36:14 +0400239 renderRegistrationSuccess(w, s.loginAddr)
Giorgi Lekveishvili12850ee2023-06-22 13:11:17 +0400240}
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400241
242type firstaccount struct {
243 Created bool `json:"created"`
244 Groups []string `json:"groups"`
245}
246
247type initRequest struct {
248 Owner string `json:"owner"`
249 Groups []string `json:"groups"`
250}
251
252func (s *Server) initMemberships(username string) error {
gio308105e2024-04-19 13:12:13 +0400253 return s.repo.Do(func(r installer.RepoFS) (string, error) {
gio3af43942024-04-16 08:13:50 +0400254 var fa firstaccount
255 if err := installer.ReadYaml(r, "first-account.yaml", &fa); err != nil {
256 return "", err
257 }
258 if fa.Created {
259 return "", nil
260 }
261 req := initRequest{username, fa.Groups}
262 var buf bytes.Buffer
263 if err := json.NewEncoder(&buf).Encode(req); err != nil {
264 return "", err
265 }
266 if _, err := http.Post(s.membershipsInitAddr, "applications/json", &buf); err != nil {
267 return "", err
268 }
269 fa.Created = true
270 if err := installer.WriteYaml(r, "first-account.yaml", fa); err != nil {
271 return "", err
272 }
273 return "initialized groups for first account", nil
274 })
Giorgi Lekveishvilid542b732024-03-25 18:17:39 +0400275}