amuse.git

commit ca4d55b8ec2f3a0146c945ab44f9a6cb55a4279d

Author: Adam <git@apiote.tk>

show signup form

 front/capnproto.go | 8 +++
 front/html.go | 15 ++++++
 front/renderer.go | 3 +
 go.mod | 1 
 go.sum | 2 
 libamuse/qr.go | 10 ++++
 libamuse/signup.go | 56 ++++++++++++++++++++++++++
 router.go | 93 +++++++++++++++++++++++++++++++++++++++++++
 static/style/style.css | 36 ++++++++++++++++
 templates/login.html | 36 ++++++++--------
 templates/signup.html | 54 +++++++++++++++++++++++++


diff --git a/front/capnproto.go b/front/capnproto.go
index d9459f01145f2671e0533b71a7297dde5baf414a..993d6a04dc0f12534dab1271a380cc5c19068407 100644
--- a/front/capnproto.go
+++ b/front/capnproto.go
@@ -6,6 +6,8 @@ 	"notabug.org/apiote/amuse/wikidata"
 	"notabug.org/apiote/amuse/datastructure"
 
 	"golang.org/x/text/language"
+
+	"github.com/pquerna/otp"
 )
 
 func TODO(message string) interface{} {
@@ -37,7 +39,6 @@
 func (CapnprotoRenderer) RenderBook(book wikidata.Book, languages []language.Tag) string {
 	return TODO("implement CapnprotoRenderer.RenderBook").(string)
 }
-
 func (CapnprotoRenderer) RenderBookSerie(bookSerie wikidata.BookSerie, languages []language.Tag) string {
 	return TODO("implement CapnprotoRenderer.RenderBookSerie").(string)
 }
@@ -53,6 +54,11 @@
 func (CapnprotoRenderer) RenderLogin(languages []language.Tag, err error, target string) string {
 	// todo throw Wrong Accept
 	return TODO("implement CapnprotoRenderer.RenderLogin").(string)
+}
+
+func (CapnprotoRenderer) RenderSignup(languages []language.Tag, err error, target *otp.Key) string {
+	// todo throw Wrong Accept
+	return TODO("implement CapnprotoRenderer.RenderSignup").(string)
 }
 
 func (CapnprotoRenderer) RenderWatchlist(watchlist datastructure.Watchlist, languages []language.Tag) string {




diff --git a/front/html.go b/front/html.go
index 032616129d9def0d580a21cca672f9dbdc0b3191..89ec021f32f5100951ae5d83b42302cfe079a183 100644
--- a/front/html.go
+++ b/front/html.go
@@ -2,17 +2,20 @@ package front
 
 import (
 	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/datastructure"
 	"notabug.org/apiote/amuse/i18n"
 	"notabug.org/apiote/amuse/tmdb"
 	"notabug.org/apiote/amuse/utils"
 	"notabug.org/apiote/amuse/wikidata"
-	"notabug.org/apiote/amuse/datastructure"
 
 	"bytes"
 	"golang.org/x/text/language"
 	"html/template"
+	"net/url"
 	"strings"
 	"time"
+
+	"github.com/pquerna/otp"
 )
 
 type RenderData struct {
@@ -136,6 +139,16 @@ func (HtmlRenderer) RenderLogin(languages []language.Tag, authError error, target string) string {
 	data := RenderData{Data: target}
 	data.State.Error = authError
 	return render(languages, data, "login")
+}
+
+func (HtmlRenderer) RenderSignup(languages []language.Tag, authError error, key *otp.Key) string {
+	secret := struct {
+		Secret string
+		Url    string
+	}{key.Secret(), url.QueryEscape(key.URL())}
+	data := RenderData{Data: secret}
+	data.State.Error = authError
+	return render(languages, data, "signup")
 }
 
 func (r HtmlRenderer) RenderWatchlist(watchlist datastructure.Watchlist, languages []language.Tag) string {




diff --git a/front/renderer.go b/front/renderer.go
index 01a3bac20fd7100d29e9badbc41f0e74dd6d54af..b0c8c39176b3a90d92243bf8849d1655c93068b5 100644
--- a/front/renderer.go
+++ b/front/renderer.go
@@ -7,6 +7,8 @@ 	"notabug.org/apiote/amuse/wikidata"
 	"notabug.org/apiote/amuse/datastructure"
 
 	"golang.org/x/text/language"
+
+	"github.com/pquerna/otp"
 )
 
 type NoSuchRendererError struct {
@@ -28,6 +30,7 @@ 	RenderBookSerie(wikidata.BookSerie, []language.Tag) string
 	RenderAbout([]language.Tag) string
 	RenderErrorPage(int, []language.Tag) string
 	RenderLogin([]language.Tag, error, string) string
+	RenderSignup([]language.Tag, error, *otp.Key) string
 	RenderWatchlist(datastructure.Watchlist, []language.Tag) string
 	RenderTvQueue(datastructure.TvQueue, []language.Tag) string
 	RenderExperiences(datastructure.Experiences, []language.Tag) string




diff --git a/go.mod b/go.mod
index 68d11258d15083bcd37c492c9302341962ea31de..bb857afb77b8b875145ee8d625f43792c7b9213d 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ 	github.com/onsi/gomega v1.7.1 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pquerna/otp v1.2.0
 	github.com/sirupsen/logrus v1.5.0 // indirect
+	github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
 	github.com/stretchr/testify v1.4.0 // indirect
 	golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5
 	golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect




diff --git a/go.sum b/go.sum
index b7b9fcb0594cf4bd5aec5328db2464e8f8b68088..46fd581a60d78651ea4deabeb756cf39e266a46b 100644
--- a/go.sum
+++ b/go.sum
@@ -83,6 +83,8 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
 github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
+github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs=
+github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
 github.com/sozorogami/gover v0.0.0-20171022184752-b58185e213c5 h1:TAPeDBsd52dRWoWzf5trgBzxzMYHTYjYI+4xNyCdoCU=
 github.com/sozorogami/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:nHNlDYIQZn44RvqH0kCpl/dMMVWXkav0QIgzGxV1Ab4=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=




diff --git a/libamuse/qr.go b/libamuse/qr.go
new file mode 100644
index 0000000000000000000000000000000000000000..095a87e459decbc8739d5d2ecd2e63e3bc381878
--- /dev/null
+++ b/libamuse/qr.go
@@ -0,0 +1,10 @@
+package libamuse
+
+import (
+	"github.com/skip2/go-qrcode"
+)
+
+func ShowQr(keyUrl string) (string, error){
+	png, err := qrcode.Encode(keyUrl, qrcode.Low, 256)  // todo webp
+	return string(png), err
+}




diff --git a/libamuse/signup.go b/libamuse/signup.go
new file mode 100644
index 0000000000000000000000000000000000000000..232ebf8ee0df634e1fa8f78d92154842caebfe31
--- /dev/null
+++ b/libamuse/signup.go
@@ -0,0 +1,56 @@
+package libamuse
+
+import (
+	"fmt"
+	
+	"github.com/pquerna/otp"
+	"github.com/pquerna/otp/totp"
+	"notabug.org/apiote/gott"
+)
+
+func createSecret(args ...interface{}) (interface{}, error) {
+	result := args[1].(*Result)
+	opts := totp.GenerateOpts{  // todo host
+		Issuer: "amuse.apiote.tk",
+		AccountName: "nearly_headless_nick@amuse.apiote.tk",
+	}
+	key, err := totp.Generate(opts)
+	result.result = key
+	return gott.Tuple(args), err
+}
+
+func renderSignup(args ...interface{}) interface{} {
+	result := args[1].(*Result)
+	secret := result.result.(*otp.Key)
+	var authError error
+	if args[2] != nil {
+		authError = args[2].(error)
+	}
+	result.page = result.renderer.RenderSignup(result.languages, authError, secret)
+	return gott.Tuple(args)
+}
+
+func ShowSignup(acceptLanguages, mimetype string, err error) (string, error) {
+	r, err := gott.
+		NewResult(gott.Tuple{&RequestData{language: acceptLanguages, mimetype: mimetype}, &Result{}, err}).
+		Bind(parseLanguage).
+		Bind(createSecret).
+		Bind(createRenderer).
+		Map(renderSignup).
+		Finish()
+
+	if err != nil {
+		return "", err
+	} else {
+		return r.(gott.Tuple)[1].(*Result).page, nil
+	}
+}
+
+func DoSignup(username, password, passwordConfirm string, sfaEnabled bool, sfaSecret, sfa string) error {
+	fmt.Println(username, password, passwordConfirm, sfaEnabled, sfaSecret, sfa)
+	return nil
+}
+
+func ShowSignedup(acceptLanguages, mimetype string) (string, error) {
+	return "", nil
+}




diff --git a/router.go b/router.go
index dbac71c674fb4f2da21e735fbd8666940a9e4ed8..1876d086fdef738ee26884952f9c96c6a4d58b43 100644
--- a/router.go
+++ b/router.go
@@ -223,6 +223,76 @@ 		loginPost(w, r, acceptLanguages, mimetype)
 	}
 }
 
+func signupGet(w http.ResponseWriter, r *http.Request, acceptLanguages, mimetype string) {
+	auth := getAuthToken(r)
+	user, _ := libamuse.VerifyAuthToken(auth)
+	if !user.IsEmpty() {
+		w.Header().Add("Location", "/")
+		w.WriteHeader(303)
+		return
+	}
+	signup, err := libamuse.ShowSignup(acceptLanguages, mimetype, nil)
+	render(signup, err, w, acceptLanguages, mimetype)
+}
+
+func signupPost(w http.ResponseWriter, r *http.Request, acceptLanguages, mimetype string) {
+	// todo check mimetype (html,capnproto)
+	r.ParseForm()
+	username := r.PostForm.Get("username")
+	password := r.PostForm.Get("password")
+	passwordConfirm := r.PostForm.Get("password2")
+	sfaEnabled := r.PostForm.Get("sfaEnabled") == "true"
+	sfaSecret := r.PostForm.Get("sfaSecret")
+	sfa := r.PostForm.Get("sfa")
+
+	err := libamuse.DoSignup(username, password, passwordConfirm, sfaEnabled, sfaSecret, sfa)
+	if err != nil {
+		fmt.Println(err)
+		if authErr, ok := err.(accounts.AuthError); ok {
+			var signup string
+			var err error
+			if mimetype == "text/html" {
+				signup, err = libamuse.ShowSignup(acceptLanguages, mimetype, &authErr)
+			} else {
+				// todo send capnproto not authed
+			}
+			render(signup, err, w, acceptLanguages, mimetype)
+		} else {
+			render("", err, w, acceptLanguages, mimetype)
+		}
+	} else {
+		if mimetype == "text/html" {
+			w.Header().Add("Location", "/signedup")
+			w.WriteHeader(303)
+		} else {
+			// todo send capnproto authed
+		}
+	}
+}
+
+func signedup(w http.ResponseWriter, r *http.Request) {
+	acceptLanguages := r.Header.Get("Accept-Language")
+	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
+
+	defer recovery(acceptLanguages, mimetype, w)
+
+	signedup, err := libamuse.ShowSignedup(acceptLanguages, mimetype)
+	render(signedup, err, w, acceptLanguages, mimetype)
+}
+
+func signup(w http.ResponseWriter, r *http.Request) {
+	acceptLanguages := r.Header.Get("Accept-Language")
+	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
+
+	defer recovery(acceptLanguages, mimetype, w)
+
+	if r.Method == "" || r.Method == "GET" {
+		signupGet(w, r, acceptLanguages, mimetype)
+	} else if r.Method == "POST" {
+		signupPost(w, r, acceptLanguages, mimetype)
+	}
+}
+
 func static(w http.ResponseWriter, r *http.Request) {
 	etagReq := r.Header.Get("If-None-Match")
 	f, err := os.Open(utils.DataHome + "/" + r.URL.Path[1:])
@@ -341,7 +411,7 @@ 		var datetime string
 		if isOtherTime {
 			date := r.PostForm.Get("watchedDate")
 			time := r.PostForm.Get("watchedTime")
-			datetime = date+"T"+time+":00"
+			datetime = date + "T" + time + ":00"
 		} else {
 			datetime = ""
 		}
@@ -389,6 +459,24 @@ 		}
 	}
 }
 
+func qr(w http.ResponseWriter, r *http.Request) {
+	query := r.URL.Query().Get("url")
+	acceptLanguages := r.Header.Get("Accept-Language")
+	mimetype := strings.Split(r.Header.Get("Accept"), ",")[0]
+	
+	keyUrl, err := url.QueryUnescape(query)
+	if err != nil {
+		render("", errors.New("400"), w, acceptLanguages, mimetype)
+	}
+
+	qr, err := libamuse.ShowQr(keyUrl)
+	w.Header().Set("Content-Type", "image/png; charset=utf-8")
+	if err != nil {
+		render("", err, w, acceptLanguages, mimetype)
+	}
+	w.Write([]byte(qr))
+}
+
 func route(port int) {
 	portStr := fmt.Sprintf(":%d", port)
 
@@ -404,6 +492,9 @@ 	http.HandleFunc("/bookseries/", bookSerie)
 	http.HandleFunc("/users/", userRouter)
 
 	http.HandleFunc("/login", login)
+	http.HandleFunc("/signup", signup)
+	http.HandleFunc("/signedup", signedup)
+	http.HandleFunc("/qr/", qr)
 	fmt.Printf("running on %s\n", portStr)
 	e := http.ListenAndServe(portStr, nil)
 	if e != nil {




diff --git a/static/style/style.css b/static/style/style.css
index 932095c6bc9ffb7fdd54d05e828a5fe6be7f2763..25fb2c011c8432d0b2dc232cae69d483a8606766 100644
--- a/static/style/style.css
+++ b/static/style/style.css
@@ -128,6 +128,14 @@ .watched-datetime-check:checked ~ .watched-box-flex {
 	display: flex;
 }
 
+#sfa-box {
+	display: none;
+}
+
+#sfa-enabled:checked ~ #sfa-box {
+	display: block;
+}
+
 * {
 	box-sizing: border-box;
 }
@@ -266,6 +274,11 @@ .padding-bottom-_25 {
 	padding-bottom: .25rem;
 }
 
+.padding-lr-_25 {
+	padding-left: .25rem;
+	padding-right: .25rem;
+}
+
 .padding-lr-_1 {
 	padding-left: .1rem;
 	padding-right: .1rem;
@@ -287,6 +300,10 @@ }
 
 /* MARGINS */
 
+.margin-auto {
+	margin: auto
+}
+
 .margin-lr-1 {
 	margin-left: 1rem;
 	margin-right: 1rem;
@@ -302,7 +319,7 @@ 	margin-top: 0;
 }
 
 .margin-top-_25 {
-	margin-top: .25rem;
+	margin-top: .25vvrem;
 }
 
 .margin-top-1 {
@@ -338,6 +355,11 @@
 .margin-tb-1 {
 	margin-top: 1rem;
 	margin-bottom: 1rem;
+}
+
+.margin-tb-2 {
+	margin-top: 2rem;
+	margin-bottom: 2rem;
 }
 
 /* WIDTH */
@@ -694,6 +716,10 @@ }
 
 /* BORDER */
 
+.border-solid {
+	border-style: solid;
+}
+
 .border-none {
 	border: none;
 }
@@ -712,6 +738,14 @@ }
 
 .border-bottom:active, .border-bottom:focus, .border-bottom:focus-within {
 	border-bottom: 1px solid var(--accent);
+}
+
+.border-_5 {
+	border-width: .5px;
+}
+
+.border-grey {
+	border-color: var(--grey);
 }
 
 .no-outline {




diff --git a/templates/login.html b/templates/login.html
index 30a865338699fefcc3869377180674b295f7cb00..1d744b57ec98258d5f70f12a4e1212cf62b2b37c 100644
--- a/templates/login.html
+++ b/templates/login.html
@@ -16,24 +16,24 @@ 			
 		</header>
 		<main class="margin-lr-1 flex-fill">
 			<div class="flex flex-column height-fill flex-centre">
-			<div class="w12 flex flex-centre border-box left">
-				<div>
-					<div class="sans italic centre">Alohomora!</div>
-					<hr/>
-					{{if .State.Error}}
-					<div class="sans bg-error centre bold margin-tb-1 padding-tb-1">Authentication error</div>
-					{{ end }}
-					<form action="/login" method="POST" class="clear-float">
-						<label for="username" class="sans block font-1 margin-top-1">Username</label>
-						<input autofocus type="text" required id="username" name="username" class="block bg-none border-none border-bottom text font-1_5" />
-						<label for="password" class="sans block font-1 margin-top-1">Pasword</label>
-						<input type="password" required id="password" name="password" class="block bg-none border-none border-bottom text font-1_5" />
-						<label for="sfa" class="sans block font-1 margin-top-1">Second factor <span title="Required if you have enabled during signup or later in your account" class="material-icon">&#xe887</span></label>
-						<input type="text" maxlength="6" pattern="[0-9]{6}" autocomplete="off" id="sfa" name="sfa" class="block bg-none border-none border-bottom text font-1_5" />
-						<input type="hidden" value="{{.Data}}" name="target" />
-						<input type="submit" class="margin-tb-1 right-float bg-accent border-text padding-lr-_5 padding-tb-_25 cursor-hand no-outline" value="Log in"/>
-					</form>
-					<p class="sans font-_875">Doesn't have an account? <a href="/signup">Sign up</a></p>
+				<div class="w12 flex flex-centre border-box left">
+					<div>
+						<div class="sans italic centre">Alohomora!</div>
+						<hr/>
+						{{if .State.Error}}
+						<div class="sans bg-error centre bold margin-tb-1 padding-tb-1">Authentication error</div>
+						{{ end }}
+						<form action="/login" method="POST" class="clear-float">
+							<label for="username" class="sans block font-1 margin-top-1">Username</label>
+							<input autofocus type="text" required id="username" name="username" class="block bg-none border-none border-bottom text font-1_5" />
+							<label for="password" class="sans block font-1 margin-top-1">Pasword</label>
+							<input type="password" required id="password" name="password" class="block bg-none border-none border-bottom text font-1_5" />
+							<label for="sfa" class="sans block font-1 margin-top-1">Second factor <span title="Required if you have enabled during signup or later in your account" class="material-icon">&#xe887</span></label>
+							<input type="text" pattern="[0-9 ]*" inputmode="numeric" autocomplete="off" id="sfa" name="sfa" class="block bg-none border-none border-bottom text font-1_5" />
+							<input type="hidden" value="{{.Data}}" name="target" />
+							<input type="submit" class="margin-tb-1 right-float bg-accent border-text padding-lr-_5 padding-tb-_25 cursor-hand no-outline" value="Log in"/>
+						</form>
+						<p class="sans font-_875">Doesn't have an account? <a href="/signup">Sign up</a></p>
 					</div>
 				</div>
 			</div>




diff --git a/templates/signup.html b/templates/signup.html
new file mode 100644
index 0000000000000000000000000000000000000000..08e59561d9066056f7f8567272faf4afab2cb369
--- /dev/null
+++ b/templates/signup.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="UTF-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<title>a·muse</title>
+		<link rel="stylesheet" href="/static/style/style.css" />
+		<link rel="icon" type="image/svg+xml" href="/static/img/logo.svg">
+		<link rel="apple-touch-icon" type="image/svg+xml" href="/static/img/logo.svg">
+	</head>
+	<body class="flex flex-column height-all">
+		<header class="w12 padding-bottom-_25 flex flex-row flex-justify-space flex-align-centre flex-content">
+			<a href="/" class="decoration-none">
+				<h1 class="inline valign-mid text sans margin-lr-1">a·muse</h1>
+			</a>
+		</header>
+		<main class="margin-lr-1 flex-fill">
+			<div class="flex flex-column height-fill flex-centre">
+				<div class="w12 flex flex-centre border-box left">
+					<div>
+						<div class="sans italic centre">Appare Vestigium!</div>
+						<hr/>
+						{{if .State.Error}}
+						<div class="sans bg-error centre bold margin-tb-1 padding-tb-1">Error</div>
+						{{ end }}
+						<form action="/signup" method="POST" class="clear-float">
+							<label for="username" class="sans block font-1 margin-top-1">Username</label>
+							<input autofocus type="text" required id="username" name="username" class="block bg-none border-none border-bottom text font-1_5" />
+							<label for="password" class="sans block font-1 margin-top-1">Pasword</label>
+							<input type="password" required id="password" name="password" class="block bg-none border-none border-bottom text font-1_5" />
+							<label for="password2" class="sans block font-1 margin-top-1">Confirm password</label>
+							<input type="password" required id="password2" name="password2" class="block bg-none border-none border-bottom text font-1_5" />
+							<div class="margin-tb-2 border-_5 border-grey padding-tb-_5 padding-lr-_25 border-solid">
+								<label for="sfa-enabled" class="sans font-1 margin-top-1">Enable second factor authentication <span title="Use Your favourite TOTP app" class="material-icon">&#xe887</span></label>
+								<input type="checkbox" id="sfa-enabled" class="" name="sfaEnabled" value="true"/>
+								<div class="" id="sfa-box">
+									<input type="hidden" name="sfaSecret" class="margin-lr-_5 margin-tb-_5 text bg-none border-none" value="{{.Data.Secret}}" />
+									<div class="margin-tb-_5">
+										<img src="/qr/?url={{.Data.Url}}" class="block margin-auto"/>
+									</div>
+									<span class="sans text-unimportant">{{.Data.Secret}}</span>
+									<label for="sfa" class="sans block font-1 margin-top-1">Confirm second factor authentication</label>
+									<input type="text" pattern="[0-9 ]*" inputmode="numeric" autocomplete="off" id="sfa" name="sfa" class="block bg-none border-none border-bottom text font-1_5" />
+								</div>
+							</div>
+							<input type="submit" class="margin-tb-1 right-float bg-accent border-text padding-lr-_5 padding-tb-_25 cursor-hand no-outline" value="Sign up"/>
+						</form>
+						<p class="sans font-_875">Already have an account? <a href="/login">Log in</a></p>
+					</div>
+				</div>
+			</div>
+		</main>
+	</body>
+</html>