installer: create headscale user resource
diff --git a/charts/headscale-user/.helmignore b/charts/headscale-user/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/charts/headscale-user/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/charts/headscale-user/Chart.yaml b/charts/headscale-user/Chart.yaml
new file mode 100644
index 0000000..2b8cdf9
--- /dev/null
+++ b/charts/headscale-user/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: headscale-user
+description: A Helm chart to create headscale users
+type: application
+version: 0.0.1
+appVersion: "0.0.1"
diff --git a/charts/headscale-user/templates/headscale-user.yaml b/charts/headscale-user/templates/headscale-user.yaml
new file mode 100644
index 0000000..8fc998f
--- /dev/null
+++ b/charts/headscale-user/templates/headscale-user.yaml
@@ -0,0 +1,11 @@
+apiVersion: headscale.dodo.cloud/v1
+kind: HeadscaleUser
+metadata:
+  name: {{ .Values.username }}
+  namespace: {{ .Release.Namespace }}
+spec:
+  headscaleAddress: {{ .Values.headscaleApiAddress }}
+  name: {{ .Values.username }}
+  preAuthKey:
+    enabled: {{ .Values.preAuthKey.enabled }}
+    secretName: {{ .Values.preAuthKey.secretName }}
diff --git a/charts/headscale-user/values.yaml b/charts/headscale-user/values.yaml
new file mode 100644
index 0000000..c91e8d8
--- /dev/null
+++ b/charts/headscale-user/values.yaml
@@ -0,0 +1,6 @@
+username: foo
+headscaleApiAddress: headscale-api.example.com
+preAuthKey:
+  enabled: false
+  secretName: foo-secret
+
diff --git a/core/headscale/main.go b/core/headscale/main.go
index b9dbc22..1ca4f6b 100644
--- a/core/headscale/main.go
+++ b/core/headscale/main.go
@@ -20,19 +20,16 @@
 // TODO(gio): ingress-private user name must be configurable
 const defaultACLs = `
 {
-  "hosts": {
-    "private-network": "10.1.0.0/24",
-  },
   "autoApprovers": {
     "routes": {
-      "private-network": ["private-network-proxy@{{ .Domain }}"],
+      "10.1.0.0/24": ["private-network-proxy@{{ .Domain }}"],
     },
   },
   "acls": [
     { // Everyone can access ingress-private service
       "action": "accept",
       "src": ["*"],
-      "dst": ["private-network:*"],
+      "dst": ["10.1.0.0/24:*"],
     },
   ],
 }
diff --git a/core/installer/app.go b/core/installer/app.go
index 3210667..4935169 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -102,6 +102,7 @@
 		CreateCertificateIssuerPrivate(valuesTmpls, tmpls),
 		CreateAppCoreAuth(valuesTmpls, tmpls),
 		CreateAppHeadscale(valuesTmpls, tmpls),
+		CreateAppHeadscaleUser(valuesTmpls, tmpls),
 		CreateAppTailscaleProxy(valuesTmpls, tmpls),
 		CreateMetallbIPAddressPool(valuesTmpls, tmpls),
 		CreateEnvManager(valuesTmpls, tmpls),
@@ -383,6 +384,22 @@
 	}
 }
 
+func CreateAppHeadscaleUser(fs embed.FS, tmpls *template.Template) App {
+	schema, err := fs.ReadFile("values-tmpl/headscale-user.jsonschema")
+	if err != nil {
+		panic(err)
+	}
+	return App{
+		"headscale-user",
+		[]string{"app-headscale"},
+		[]*template.Template{
+			tmpls.Lookup("headscale-user.yaml"),
+		},
+		string(schema),
+		tmpls.Lookup("headscale-user.md"),
+	}
+}
+
 func CreateAppTailscaleProxy(fs embed.FS, tmpls *template.Template) App {
 	schema, err := fs.ReadFile("values-tmpl/tailscale-proxy.jsonschema")
 	if err != nil {
diff --git a/core/installer/values-tmpl/headscale-user.jsonschema b/core/installer/values-tmpl/headscale-user.jsonschema
new file mode 100644
index 0000000..e4884a6
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-user.jsonschema
@@ -0,0 +1,14 @@
+{
+  "type": "object",
+  "properties": {
+    "Username": { "type": "string" },
+	"PreAuthKey": {
+	  "type": "object",
+	  "properties": {
+	    "Enabled": { "type": "boolean" }
+	  },
+	  "additionalProperties": false
+	}
+  },
+  "additionalProperties": false
+}
diff --git a/core/installer/values-tmpl/headscale-user.md b/core/installer/values-tmpl/headscale-user.md
new file mode 100644
index 0000000..7246082
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-user.md
@@ -0,0 +1 @@
+Creates Headscale user resource
diff --git a/core/installer/values-tmpl/headscale-user.yaml b/core/installer/values-tmpl/headscale-user.yaml
new file mode 100644
index 0000000..0eca8d0
--- /dev/null
+++ b/core/installer/values-tmpl/headscale-user.yaml
@@ -0,0 +1,20 @@
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: user-{{ .Values.Username }}
+  namespace: {{ .Release.Namespace }}
+spec:
+  chart:
+    spec:
+      chart: charts/headscale-user
+      sourceRef:
+        kind: GitRepository
+        name: pcloud
+        namespace: {{ .Global.Id }}
+  interval: 1m0s
+  values:
+    username: {{ .Values.Username }}
+    headscaleApiAddress: http://headscale-api.{{ .Global.Id }}-app-headscale.svc.cluster.local
+    preAuthKey:
+      enabled: {{ .Values.PreAuthKey.Enabled }}
+      secretName: {{ .Values.Username }}-headscale-preauthkey
diff --git a/core/installer/values-tmpl/tailscale-proxy.yaml b/core/installer/values-tmpl/tailscale-proxy.yaml
index 1774f1e..904f850 100644
--- a/core/installer/values-tmpl/tailscale-proxy.yaml
+++ b/core/installer/values-tmpl/tailscale-proxy.yaml
@@ -16,7 +16,7 @@
         namespace: {{ .Global.Id }}
   interval: 1m0s
   values:
-    hostname: {{ .Global.Id }}-{{ .Values.HostnameSuffix }}
+    hostname: {{ .Values.Hostname}}
     apiServer: http://headscale-api.{{ .Global.Id }}-app-headscale.svc.cluster.local
     loginServer: https://headscale.{{ .Global.Domain }} # TODO(gio): take headscale subdomain from configuration
     ipSubnet: {{ .Values.IPSubnet }}
diff --git a/core/installer/welcome/create-admin-account.html b/core/installer/welcome/create-admin-account.html
index 5f6efd2..2b22074 100644
--- a/core/installer/welcome/create-admin-account.html
+++ b/core/installer/welcome/create-admin-account.html
@@ -36,7 +36,7 @@
 				aria-label="Secret Token"
 				required
 			  />
-			  <button type="submit" class="contrast">Create Account</button>
+			  <button type="submit">Create Account</button>
 			</form>
 		</main>
 	</body>
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 4e74181..3d3e332 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -556,9 +556,9 @@
 			return err
 		}
 		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"Username":       "private-network-proxy",
-			"IPSubnet":       "10.1.0.0/24",
-			"HostnameSuffix": "private-network-proxy",
+			"Hostname": "private-network-proxy",
+			"Username": "private-network-proxy",
+			"IPSubnet": "10.1.0.0/24",
 		}); err != nil {
 			return err
 		}
diff --git a/core/installer/welcome/welcome.go b/core/installer/welcome/welcome.go
index 2517a6f..e13ec3a 100644
--- a/core/installer/welcome/welcome.go
+++ b/core/installer/welcome/welcome.go
@@ -93,7 +93,11 @@
 }
 
 func (s *Server) createAdminAccount(w http.ResponseWriter, r *http.Request) {
-	var req createAccountReq
+	req, err := extractReq(r)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	// TODO(giolekva): accounts-ui create user req
 	{
 		config, err := s.repo.ReadConfig()
@@ -127,19 +131,20 @@
 			}
 		}
 		{
-			app, err := appsRepo.Find("tailscale-proxy")
+			app, err := appsRepo.Find("headscale-user")
 			if err != nil {
 				http.Error(w, err.Error(), http.StatusInternalServerError)
 				return
 			}
 			if err := appManager.Install(*app, nsGen, suffixGen, map[string]any{
 				"Username": req.Username,
-				"IPSubnet": "10.1.0.0/24", // TODO(giolekva): this should be taken from the config generated during new env creation
+				"PreAuthKey": map[string]any{
+					"Enabled": false,
+				},
 			}); err != nil {
 				http.Error(w, err.Error(), http.StatusInternalServerError)
 				return
 			}
-			// TODO(giolekva): headscale accept routes
 		}
 	}
 	if _, err := w.Write([]byte("OK")); err != nil {