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"></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"></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"></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>