Auth: registration/login/logout/whoami
diff --git a/core/auth/ui/Dockerfile b/core/auth/ui/Dockerfile
new file mode 100644
index 0000000..8a76b08
--- /dev/null
+++ b/core/auth/ui/Dockerfile
@@ -0,0 +1,4 @@
+FROM alpine:latest
+
+COPY server /usr/bin
+RUN chmod +x /usr/bin/server
diff --git a/core/auth/ui/Makefile b/core/auth/ui/Makefile
new file mode 100644
index 0000000..c444c97
--- /dev/null
+++ b/core/auth/ui/Makefile
@@ -0,0 +1,18 @@
+build:
+	go build -o server *.go
+
+clean:
+	rm -f server
+
+image: clean build
+	docker build --tag=giolekva/auth-ui .
+
+push: image
+	docker push giolekva/auth-ui:latest
+
+
+push_arm64: export GOOS=linux
+push_arm64: export GOARCH=arm64
+push_arm64: export CGO_ENABLED=0
+push_arm64: export GO111MODULE=on
+push_arm64: push
diff --git a/core/auth/ui/go.mod b/core/auth/ui/go.mod
new file mode 100644
index 0000000..49c33b1
--- /dev/null
+++ b/core/auth/ui/go.mod
@@ -0,0 +1,8 @@
+module github.com/giolekva/pcloud/core/auth/ui
+
+go 1.16
+
+require (
+	github.com/gorilla/mux v1.8.0
+	github.com/itaysk/regogo v0.0.0-20200423164851-e9433c1fe5a7
+)
diff --git a/core/auth/ui/go.sum b/core/auth/ui/go.sum
new file mode 100644
index 0000000..69c993e
--- /dev/null
+++ b/core/auth/ui/go.sum
@@ -0,0 +1,73 @@
+github.com/Jeffail/gabs/v2 v2.5.0 h1:ERXffrksCEPjKVDWbZDBcOwrpXctXfeFGXxOQh1umOE=
+github.com/Jeffail/gabs/v2 v2.5.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/OneOfOne/xxhash v1.2.7 h1:fzrmmkskv067ZQbd9wERNGuxckWw67dyzoMG62p7LMo=
+github.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4 h1:bRzFpEzvausOAt4va+I/22BZ1vXDtERngp0BNYDKej0=
+github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/protobuf v0.0.0-20181025225059-d3de96c4c28e/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
+github.com/gorilla/mux v0.0.0-20181024020800-521ea7b17d02/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/itaysk/regogo v0.0.0-20200423164851-e9433c1fe5a7 h1:I9EDcKhL1c3bU0GvVvxU87o+fAR9XAwCf+bDd+wxY5U=
+github.com/itaysk/regogo v0.0.0-20200423164851-e9433c1fe5a7/go.mod h1:QB9wWkHRsl6HnoMWvVMIzf07Wj7WtSW5IInTjfvnjek=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mna/pigeon v0.0.0-20180808201053-bb0192cfc2ae/go.mod h1:Iym28+kJVnC1hfQvv5MUtI6AiFFzvQjHcvI4RFTG/04=
+github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/open-policy-agent/opa v0.18.0 h1:EC81mO3/517Kq5brJHydqKE5MLzJ+4cdJvUQKxLzHy8=
+github.com/open-policy-agent/opa v0.18.0/go.mod h1:6pC1cMYDI92i9EY/GoA2m+HcZlcCrh3jbfny5F7JVTA=
+github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
+github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0 h1:R+lX9nKwNd1n7UE5SQAyoorREvRn3aLF6ZndXBoIWqY=
+github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.0.0-20181025174421-f30f42803563/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v0.0.0-20181024212040-082b515c9490/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
+github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
+github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b h1:vVRagRXf67ESqAb72hG2C/ZwI8NtJF2u2V76EsuOHGY=
+github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/lint v0.0.0-20181023182221-1baf3a9d7d67/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/core/auth/ui/install.yaml b/core/auth/ui/install.yaml
new file mode 100644
index 0000000..ec82b9a
--- /dev/null
+++ b/core/auth/ui/install.yaml
@@ -0,0 +1,82 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: core-auth
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: kratos-selfservice-ui
+  namespace: core-auth
+spec:
+  type: ClusterIP
+  selector:
+    app: kratos-selfservice-ui
+  ports:
+  - name: http
+    port: 80
+    targetPort: http
+    protocol: TCP
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: ingress-kratos-selfservice-ui-public
+  namespace: core-auth
+  annotations:
+    cert-manager.io/cluster-issuer: "letsencrypt-prod"
+    acme.cert-manager.io/http01-edit-in-place: "true"
+spec:
+  ingressClassName: nginx
+  tls:
+  - hosts:
+    - accounts-ui.lekva.me
+    secretName: cert-accounts-ui.lekva.me
+  rules:
+  - host: accounts-ui.lekva.me
+    http:
+      paths:
+      - path: /
+        pathType: Prefix
+        backend:
+          service:
+            name: kratos-selfservice-ui
+            port:
+              name: http
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: kratos-selfservice-ui
+  namespace: core-auth
+spec:
+  selector:
+    matchLabels:
+      app: kratos-selfservice-ui
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        app: kratos-selfservice-ui
+    spec:
+      containers:
+      - name: server
+        # image: giolekva/ory-kratos-selfservice-ui:latest
+        image: giolekva/auth-ui:latest
+        imagePullPolicy: Always
+        env:
+        - name: KRATOS_PUBLIC_URL
+          value: "https://accounts.lekva.me"
+        ports:
+        - name: http
+          containerPort: 8080
+          protocol: TCP
+        command: ["server", "--port=8080"]
+        # resources:
+        #   requests:
+        #     memory: "10Mi"
+        #     cpu: "10m"
+        #   limits:
+        #     memory: "20Mi"
+        #     cpu: "100m"
diff --git a/core/auth/ui/main.go b/core/auth/ui/main.go
new file mode 100644
index 0000000..0546802
--- /dev/null
+++ b/core/auth/ui/main.go
@@ -0,0 +1,344 @@
+package main
+
+import (
+	"bytes"
+	"embed"
+	"encoding/json"
+	"errors"
+	"flag"
+	"fmt"
+	"html/template"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/cookiejar"
+	"net/url"
+
+	"github.com/gorilla/mux"
+	"github.com/itaysk/regogo"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on")
+var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
+
+var ErrNotLoggedIn = errors.New("Not logged in")
+
+//go:embed templates/*
+var tmpls embed.FS
+
+type Templates struct {
+	WhoAmI       *template.Template
+	Registration *template.Template
+	Login        *template.Template
+}
+
+func ParseTemplates(fs embed.FS) (*Templates, error) {
+	registration, err := template.ParseFS(fs, "templates/registration.html")
+	if err != nil {
+		return nil, err
+	}
+	login, err := template.ParseFS(fs, "templates/login.html")
+	if err != nil {
+		return nil, err
+	}
+	whoami, err := template.ParseFS(fs, "templates/whoami.html")
+	if err != nil {
+		return nil, err
+	}
+	return &Templates{whoami, registration, login}, nil
+}
+
+type Server struct {
+	kratos string
+	tmpls  *Templates
+}
+
+func (s *Server) Start(port int) error {
+	r := mux.NewRouter()
+	http.Handle("/", r)
+	r.Path("/registration").Methods(http.MethodGet).HandlerFunc(s.registrationInitiate)
+	r.Path("/registration").Methods(http.MethodPost).HandlerFunc(s.registration)
+	r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
+	r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
+	r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
+	r.Path("/").HandlerFunc(s.whoami)
+	fmt.Printf("Starting HTTP server on port: %d\n", port)
+	return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
+}
+
+func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		return "", err
+	}
+	client := &http.Client{
+		Jar: jar,
+	}
+	b, err := url.Parse("https://accounts.lekva.me/self-service/" + flowType + "/browser")
+	if err != nil {
+		return "", err
+	}
+	client.Jar.SetCookies(b, cookies)
+	resp, err := client.Get(fmt.Sprintf("https://accounts.lekva.me/self-service/"+flowType+"/flows?id=%s", flow))
+	if err != nil {
+		return "", err
+	}
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
+	if err != nil {
+		return "", err
+	}
+	return token.String(), nil
+}
+
+func (s *Server) registrationInitiate(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	flow, ok := r.Form["flow"]
+	if !ok {
+		http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
+		return
+	}
+	csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	log.Println(csrfToken)
+	w.Header().Set("Content-Type", "text/html")
+	if err := s.tmpls.Registration.Execute(w, csrfToken); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+type regReq struct {
+	CSRFToken string       `json:"csrf_token"`
+	Method    string       `json:"method"`
+	Password  string       `json:"password"`
+	Traits    regReqTraits `json:"traits"`
+}
+
+type regReqTraits struct {
+	Username string `json:"username"`
+}
+
+func (s *Server) registration(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	flow, ok := r.Form["flow"]
+	if !ok {
+		http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
+		return
+	}
+	req := regReq{
+		CSRFToken: r.FormValue("csrf_token"),
+		Method:    "password",
+		Password:  r.FormValue("password"),
+		Traits: regReqTraits{
+			Username: r.FormValue("username"),
+		},
+	}
+	var reqBody bytes.Buffer
+	if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	} else {
+		for _, c := range resp.Cookies() {
+			http.SetCookie(w, c)
+		}
+		http.Redirect(w, r, "/", http.StatusSeeOther)
+	}
+}
+
+// Login flow
+
+func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	flow, ok := r.Form["flow"]
+	if !ok {
+		http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
+		return
+	}
+	csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	log.Println(csrfToken)
+	w.Header().Set("Content-Type", "text/html")
+	if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
+type loginReq struct {
+	CSRFToken string `json:"csrf_token"`
+	Method    string `json:"method"`
+	Password  string `json:"password"`
+	Username  string `json:"password_identifier"`
+}
+
+func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		return nil, err
+	}
+	client := &http.Client{
+		Jar: jar,
+	}
+	b, err := url.Parse("https://accounts.lekva.me/self-service/" + flowType + "/browser")
+	if err != nil {
+		return nil, err
+	}
+	client.Jar.SetCookies(b, cookies)
+	resp, err := client.Post(fmt.Sprintf("https://accounts.lekva.me/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
+	if err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+type logoutResp struct {
+	LogoutURL string `json:"logout_url"`
+}
+
+func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		return "", err
+	}
+	client := &http.Client{
+		Jar: jar,
+	}
+	b, err := url.Parse("https://accounts.lekva.me/self-service/logout/browser")
+	if err != nil {
+		return "", err
+	}
+	client.Jar.SetCookies(b, cookies)
+	resp, err := client.Get("https://accounts.lekva.me/self-service/logout/browser")
+	if err != nil {
+		return "", err
+	}
+	var lr logoutResp
+	if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
+		return "", err
+	}
+	return lr.LogoutURL, nil
+}
+
+func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
+	jar, err := cookiejar.New(nil)
+	if err != nil {
+		return "", err
+	}
+	client := &http.Client{
+		Jar: jar,
+	}
+	b, err := url.Parse("https://accounts.lekva.me/sessions/whoami")
+	if err != nil {
+		return "", err
+	}
+	client.Jar.SetCookies(b, cookies)
+	resp, err := client.Get("https://accounts.lekva.me/sessions/whoami")
+	if err != nil {
+		return "", err
+	}
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	username, err := regogo.Get(string(respBody), "input.identity.traits.username")
+	if err != nil {
+		return "", err
+	}
+	if username.String() == "" {
+		return "", ErrNotLoggedIn
+	}
+	return username.String(), nil
+
+}
+
+func (s *Server) login(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	flow, ok := r.Form["flow"]
+	if !ok {
+		http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
+		return
+	}
+	req := loginReq{
+		CSRFToken: r.FormValue("csrf_token"),
+		Method:    "password",
+		Password:  r.FormValue("password"),
+		Username:  r.FormValue("username"),
+	}
+	var reqBody bytes.Buffer
+	if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	} else {
+		for _, c := range resp.Cookies() {
+			http.SetCookie(w, c)
+		}
+		http.Redirect(w, r, "/", http.StatusSeeOther)
+	}
+}
+
+func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
+	if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	} else {
+		http.Redirect(w, r, logoutURL, http.StatusSeeOther)
+	}
+}
+
+func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
+	if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
+		if errors.Is(err, ErrNotLoggedIn) {
+			http.Redirect(w, r, "/login", http.StatusSeeOther)
+			return
+		}
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	} else {
+		if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+		}
+	}
+}
+
+func main() {
+	flag.Parse()
+	t, err := ParseTemplates(tmpls)
+	if err != nil {
+		log.Fatal(err)
+	}
+	s := &Server{
+		kratos: *kratos,
+		tmpls:  t,
+	}
+	log.Fatal(s.Start(*port))
+}
diff --git a/core/auth/ui/server b/core/auth/ui/server
new file mode 100755
index 0000000..042afdd
--- /dev/null
+++ b/core/auth/ui/server
Binary files differ
diff --git a/core/auth/ui/templates/login.html b/core/auth/ui/templates/login.html
new file mode 100644
index 0000000..04d0435
--- /dev/null
+++ b/core/auth/ui/templates/login.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <title>Login</title>
+</head>
+<body>
+    <a href="/">whoami</a>
+    <a href="/login">login</a>
+    <a href="/logout">logout</a>
+    <a href="/registration">registration</a><br/>
+    <form action="" method="POST">
+	<label for="username">Username:</label><br />
+	<input type="text" name="username" /><br />
+	<label for="password">Password:</label><br />
+	<input type="password" name="password" /><br />
+	<input type="hidden" name="csrf_token" value="{{.}}" /><br />
+	<input type="submit" value="Login" />
+    </form>
+</body>
+</html>
+
diff --git a/core/auth/ui/templates/registration.html b/core/auth/ui/templates/registration.html
new file mode 100644
index 0000000..6a10af8
--- /dev/null
+++ b/core/auth/ui/templates/registration.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <title>Create New Account</title>
+</head>
+<body>
+    <a href="/">whoami</a>
+    <a href="/login">login</a>
+    <a href="/logout">logout</a>
+    <a href="/registration">registration</a><br/>
+    <form action="" method="POST">
+	<label for="username">Username:</label><br />
+	<input type="text" name="username" /><br />
+	<label for="password">Password:</label><br />
+	<input type="password" name="password" /><br />
+	<input type="hidden" name="csrf_token" value="{{.}}" /><br />
+	<input type="submit" value="Create New Account" />
+    </form>
+</body>
+</html>
+
diff --git a/core/auth/ui/templates/whoami.html b/core/auth/ui/templates/whoami.html
new file mode 100644
index 0000000..58a007b
--- /dev/null
+++ b/core/auth/ui/templates/whoami.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8" />
+    <title>Create New Account</title>
+</head>
+<body>
+    <a href="/">whoami</a>
+    <a href="/login">login</a>
+    <a href="/logout">logout</a>
+    <a href="/registration">registration</a><br/>
+    Hello {{.}}!
+</body>
+</html>
+