Auth: registration/login/logout/whoami
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))
+}