mirror of https://github.com/writeas/writefreely
parent
a4e373065c
commit
6429d495a2
@ -0,0 +1,209 @@ |
|||||||
|
package writefreely |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/hex" |
||||||
|
"fmt" |
||||||
|
"github.com/writeas/impart" |
||||||
|
"github.com/writeas/web-core/auth" |
||||||
|
"github.com/writeas/web-core/log" |
||||||
|
"github.com/writeas/writefreely/page" |
||||||
|
"html/template" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
type viewOauthSignupVars struct { |
||||||
|
page.StaticPage |
||||||
|
To string |
||||||
|
Message template.HTML |
||||||
|
Flashes []template.HTML |
||||||
|
|
||||||
|
AccessToken string |
||||||
|
TokenUsername string |
||||||
|
TokenAlias string |
||||||
|
TokenEmail string |
||||||
|
TokenRemoteUser string |
||||||
|
Provider string |
||||||
|
ClientID string |
||||||
|
TokenHash string |
||||||
|
|
||||||
|
Username string |
||||||
|
Alias string |
||||||
|
Email string |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
oauthParamAccessToken = "access_token" |
||||||
|
oauthParamTokenUsername = "token_username" |
||||||
|
oauthParamTokenAlias = "token_alias" |
||||||
|
oauthParamTokenEmail = "token_email" |
||||||
|
oauthParamTokenRemoteUserID = "token_remote_user" |
||||||
|
oauthParamClientID = "client_id" |
||||||
|
oauthParamProvider = "provider" |
||||||
|
oauthParamHash = "signature" |
||||||
|
oauthParamUsername = "username" |
||||||
|
oauthParamAlias = "alias" |
||||||
|
oauthParamEmail = "email" |
||||||
|
oauthParamPassword = "password" |
||||||
|
) |
||||||
|
|
||||||
|
type oauthSignupPageParams struct { |
||||||
|
AccessToken string |
||||||
|
TokenUsername string |
||||||
|
TokenAlias string |
||||||
|
TokenEmail string |
||||||
|
TokenRemoteUser string |
||||||
|
ClientID string |
||||||
|
Provider string |
||||||
|
TokenHash string |
||||||
|
} |
||||||
|
|
||||||
|
func (p oauthSignupPageParams) HashTokenParams(key string) string { |
||||||
|
hasher := sha256.New() |
||||||
|
hasher.Write([]byte(key)) |
||||||
|
hasher.Write([]byte(p.AccessToken)) |
||||||
|
hasher.Write([]byte(p.TokenUsername)) |
||||||
|
hasher.Write([]byte(p.TokenAlias)) |
||||||
|
hasher.Write([]byte(p.TokenEmail)) |
||||||
|
hasher.Write([]byte(p.TokenRemoteUser)) |
||||||
|
hasher.Write([]byte(p.ClientID)) |
||||||
|
hasher.Write([]byte(p.Provider)) |
||||||
|
return hex.EncodeToString(hasher.Sum(nil)) |
||||||
|
} |
||||||
|
|
||||||
|
func (h oauthHandler) viewOauthSignup(app *App, w http.ResponseWriter, r *http.Request) error { |
||||||
|
tp := &oauthSignupPageParams{ |
||||||
|
AccessToken: r.FormValue(oauthParamAccessToken), |
||||||
|
TokenUsername: r.FormValue(oauthParamTokenUsername), |
||||||
|
TokenAlias: r.FormValue(oauthParamTokenAlias), |
||||||
|
TokenEmail: r.FormValue(oauthParamTokenEmail), |
||||||
|
TokenRemoteUser: r.FormValue(oauthParamTokenRemoteUserID), |
||||||
|
ClientID: r.FormValue(oauthParamClientID), |
||||||
|
Provider: r.FormValue(oauthParamProvider), |
||||||
|
} |
||||||
|
if tp.HashTokenParams(h.Config.Server.HashSeed) != r.FormValue(oauthParamHash) { |
||||||
|
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Request has been tampered with."} |
||||||
|
} |
||||||
|
tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed) |
||||||
|
if err := h.validateOauthSignup(r); err != nil { |
||||||
|
return h.showOauthSignupPage(app, w, r, tp, err) |
||||||
|
} |
||||||
|
|
||||||
|
hashedPass, err := auth.HashPass([]byte(r.FormValue(oauthParamPassword))) |
||||||
|
if err != nil { |
||||||
|
return h.showOauthSignupPage(app, w, r, tp, fmt.Errorf("unable to hash password")) |
||||||
|
} |
||||||
|
newUser := &User{ |
||||||
|
Username: r.FormValue(oauthParamUsername), |
||||||
|
HashedPass: hashedPass, |
||||||
|
HasPass: true, |
||||||
|
Email: prepareUserEmail(r.FormValue(oauthParamEmail), h.EmailKey), |
||||||
|
Created: time.Now().Truncate(time.Second).UTC(), |
||||||
|
} |
||||||
|
displayName := r.FormValue(oauthParamAlias) |
||||||
|
if len(displayName) == 0 { |
||||||
|
displayName = r.FormValue(oauthParamUsername) |
||||||
|
} |
||||||
|
|
||||||
|
err = h.DB.CreateUser(h.Config, newUser, displayName) |
||||||
|
if err != nil { |
||||||
|
return h.showOauthSignupPage(app, w, r, tp, err) |
||||||
|
} |
||||||
|
|
||||||
|
err = h.DB.RecordRemoteUserID(r.Context(), newUser.ID, r.FormValue(oauthParamTokenRemoteUserID), r.FormValue(oauthParamProvider), r.FormValue(oauthParamClientID), r.FormValue(oauthParamAccessToken)) |
||||||
|
if err != nil { |
||||||
|
return h.showOauthSignupPage(app, w, r, tp, err) |
||||||
|
} |
||||||
|
|
||||||
|
if err := loginOrFail(h.Store, w, r, newUser); err != nil { |
||||||
|
return h.showOauthSignupPage(app, w, r, tp, err) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h oauthHandler) validateOauthSignup(r *http.Request) error { |
||||||
|
username := r.FormValue(oauthParamUsername) |
||||||
|
if len(username) < 5 { |
||||||
|
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too short."} |
||||||
|
} |
||||||
|
if len(username) > 20 { |
||||||
|
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too long."} |
||||||
|
} |
||||||
|
alias := r.FormValue(oauthParamAlias) |
||||||
|
if len(alias) < 5 { |
||||||
|
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Alias is too short."} |
||||||
|
} |
||||||
|
if len(alias) > 20 { |
||||||
|
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Alias is too long."} |
||||||
|
} |
||||||
|
password := r.FormValue("password") |
||||||
|
if len(password) < 5 { |
||||||
|
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Password is too short."} |
||||||
|
} |
||||||
|
email := r.FormValue(oauthParamEmail) |
||||||
|
if len(email) > 0 { |
||||||
|
parts := strings.Split(email, "@") |
||||||
|
if len(parts) != 2 || (len(parts[0]) < 1 || len(parts[1]) < 1) { |
||||||
|
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Invalid email address"} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (h oauthHandler) showOauthSignupPage(app *App, w http.ResponseWriter, r *http.Request, tp *oauthSignupPageParams, errMsg error) error { |
||||||
|
username := tp.TokenUsername |
||||||
|
alias := tp.TokenAlias |
||||||
|
email := tp.TokenEmail |
||||||
|
|
||||||
|
session, err := app.sessionStore.Get(r, cookieName) |
||||||
|
if err != nil { |
||||||
|
// Ignore this
|
||||||
|
log.Error("Unable to get session; ignoring: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
if tmpValue := r.FormValue(oauthParamUsername); len(tmpValue) > 0 { |
||||||
|
username = tmpValue |
||||||
|
} |
||||||
|
if tmpValue := r.FormValue(oauthParamAlias); len(tmpValue) > 0 { |
||||||
|
alias = tmpValue |
||||||
|
} |
||||||
|
if tmpValue := r.FormValue(oauthParamEmail); len(tmpValue) > 0 { |
||||||
|
email = tmpValue |
||||||
|
} |
||||||
|
|
||||||
|
p := &viewOauthSignupVars{ |
||||||
|
StaticPage: pageForReq(app, r), |
||||||
|
To: r.FormValue("to"), |
||||||
|
Flashes: []template.HTML{}, |
||||||
|
|
||||||
|
AccessToken: tp.AccessToken, |
||||||
|
TokenUsername: tp.TokenUsername, |
||||||
|
TokenAlias: tp.TokenAlias, |
||||||
|
TokenEmail: tp.TokenEmail, |
||||||
|
TokenRemoteUser: tp.TokenRemoteUser, |
||||||
|
Provider: tp.Provider, |
||||||
|
ClientID: tp.ClientID, |
||||||
|
TokenHash: tp.TokenHash, |
||||||
|
|
||||||
|
Username: username, |
||||||
|
Alias: alias, |
||||||
|
Email: email, |
||||||
|
} |
||||||
|
|
||||||
|
// Display any error messages
|
||||||
|
flashes, _ := getSessionFlashes(app, w, r, session) |
||||||
|
for _, flash := range flashes { |
||||||
|
p.Flashes = append(p.Flashes, template.HTML(flash)) |
||||||
|
} |
||||||
|
if errMsg != nil { |
||||||
|
p.Flashes = append(p.Flashes, template.HTML(errMsg.Error())) |
||||||
|
} |
||||||
|
err = pages["signup-oauth.tmpl"].ExecuteTemplate(w, "base", p) |
||||||
|
if err != nil { |
||||||
|
log.Error("Unable to render signup-oauth: %v", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -1,163 +1,38 @@ |
|||||||
{{define "head"}} |
{{define "head"}}<title>Log in — {{.SiteName}}</title> |
||||||
<title>Sign up — {{.SiteName}}</title> |
<meta name="description" content="Log in to {{.SiteName}}."> |
||||||
|
<meta itemprop="description" content="Log in to {{.SiteName}}."> |
||||||
<style type="text/css"> |
<style>input{margin-bottom:0.5em;}</style> |
||||||
h2 { |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
#pricing.content-container div.form-container #payment-form { |
|
||||||
display: block !important; |
|
||||||
} |
|
||||||
#pricing #signup-form table { |
|
||||||
max-width: inherit !important; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
#pricing #payment-form table { |
|
||||||
margin-top: 0 !important; |
|
||||||
max-width: inherit !important; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
tr.subscription { |
|
||||||
border-spacing: 0; |
|
||||||
} |
|
||||||
#pricing.content-container tr.subscription button { |
|
||||||
margin-top: 0 !important; |
|
||||||
margin-bottom: 0 !important; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
#pricing tr.subscription td { |
|
||||||
padding: 0 0.5em; |
|
||||||
} |
|
||||||
#pricing table.billing > tbody > tr > td:first-child { |
|
||||||
vertical-align: middle !important; |
|
||||||
} |
|
||||||
.billing-section { |
|
||||||
display: none; |
|
||||||
} |
|
||||||
.billing-section.bill-me { |
|
||||||
display: table-row; |
|
||||||
} |
|
||||||
#btn-create { |
|
||||||
color: white !important; |
|
||||||
} |
|
||||||
#total-price { |
|
||||||
padding-left: 0.5em; |
|
||||||
} |
|
||||||
#alias-site.demo { |
|
||||||
color: #999; |
|
||||||
} |
|
||||||
#alias-site { |
|
||||||
text-align: left; |
|
||||||
margin: 0.5em 0; |
|
||||||
} |
|
||||||
form dd { |
|
||||||
margin: 0; |
|
||||||
} |
|
||||||
</style> |
|
||||||
{{end}} |
{{end}} |
||||||
{{define "content"}} |
{{define "content"}} |
||||||
<div id="pricing" class="content-container wide-form"> |
<div class="tight content-container"> |
||||||
|
<h1>Log in to {{.SiteName}}</h1> |
||||||
<div class="row"> |
|
||||||
<div style="margin: 0 auto; max-width: 25em;"> |
{{if .Flashes}}<ul class="errors"> |
||||||
<h1>Sign up</h1> |
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} |
||||||
|
</ul>{{end}} |
||||||
{{ if .Error }} |
|
||||||
<p style="font-style: italic">{{.Error}}</p> |
<form action="/oauth/signup" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()"> |
||||||
{{ else }} |
<input type="hidden" name="access_token" value="{{ .AccessToken }}" /> |
||||||
{{if .Flashes}}<ul class="errors"> |
<input type="hidden" name="token_username" value="{{ .TokenUsername }}" /> |
||||||
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} |
<input type="hidden" name="token_alias" value="{{ .TokenAlias }}" /> |
||||||
</ul>{{end}} |
<input type="hidden" name="token_email" value="{{ .TokenEmail }}" /> |
||||||
|
<input type="hidden" name="token_remote_user" value="{{ .TokenRemoteUser }}" /> |
||||||
|
<input type="hidden" name="provider" value="{{ .Provider }}" /> |
||||||
|
<input type="hidden" name="client_id" value="{{ .ClientID }}" /> |
||||||
|
<input type="hidden" name="signature" value="{{ .TokenHash }}" /> |
||||||
|
|
||||||
|
<input type="text" name="username" placeholder="Username" value="{{.Username}}" /><br /> |
||||||
|
<input type="text" name="alias" placeholder="Alias"{{ if .Alias }} value="{{.Alias}}"{{ end }} /><br /> |
||||||
|
<input type="text" name="email" placeholder="Email"{{ if .Email }} value="{{.Email}}"{{ end }} /><br /> |
||||||
|
<input type="password" name="password" placeholder="Password" /><br /> |
||||||
|
<input type="submit" id="btn-login" value="Login" /> |
||||||
|
</form> |
||||||
|
|
||||||
<div id="billing"> |
|
||||||
<form action="/auth/signup-oauth" method="POST" id="signup-form" onsubmit="return signup()"> |
|
||||||
<input type="hidden" name="provider" value="{{.Provider}}" /> |
|
||||||
<input type="hidden" name="access_token" value="{{.AccessToken}}" /> |
|
||||||
<dl class="billing"> |
|
||||||
<label> |
|
||||||
<dt>Username</dt> |
|
||||||
<dd> |
|
||||||
<input type="text" id="alias" name="alias" style="width: 100%; box-sizing: border-box;" tabindex="1" autofocus /> |
|
||||||
{{if .Federation}}<p id="alias-site" class="demo">@<strong>your-username</strong>@{{.FriendlyHost}}</p>{{else}}<p id="alias-site" class="demo">{{.FriendlyHost}}/<strong>your-username</strong></p>{{end}} |
|
||||||
</dd> |
|
||||||
</label> |
|
||||||
<label> |
|
||||||
<dt>Email (optional)</dt> |
|
||||||
<dd><input type="email" name="email" id="email" style="letter-spacing: 1px; width: 100%; box-sizing: border-box;" placeholder="me@example.com" tabindex="3" /></dd> |
|
||||||
</label> |
|
||||||
<dt> |
|
||||||
<button id="btn-create" type="submit" style="margin-top: 0">Create blog</button> |
|
||||||
</dt> |
|
||||||
</dl> |
|
||||||
</form> |
|
||||||
</div> |
|
||||||
{{ end }} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<script type="text/javascript" src="/js/h.js"></script> |
|
||||||
<script type="text/javascript"> |
<script type="text/javascript"> |
||||||
function signup() { |
function disableSubmit() { |
||||||
// Validate input |
var $btn = document.getElementById("btn-login"); |
||||||
if (!aliasOK) { |
$btn.value = "Logging in..."; |
||||||
var $a = $alias; |
|
||||||
$a.el.className = 'error'; |
|
||||||
$a.el.focus(); |
|
||||||
$a.el.scrollIntoView(); |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
var $btn = document.getElementById('btn-create'); |
|
||||||
$btn.disabled = true; |
$btn.disabled = true; |
||||||
$btn.value = 'Creating...'; |
|
||||||
return true; |
|
||||||
} |
} |
||||||
|
|
||||||
var $alias = H.getEl('alias'); |
|
||||||
var $aliasSite = document.getElementById('alias-site'); |
|
||||||
var aliasOK = true; |
|
||||||
var typingTimer; |
|
||||||
var doneTypingInterval = 750; |
|
||||||
var doneTyping = function() { |
|
||||||
// Check on username |
|
||||||
var alias = $alias.el.value; |
|
||||||
if (alias != "") { |
|
||||||
var params = { |
|
||||||
username: alias |
|
||||||
}; |
|
||||||
var http = new XMLHttpRequest(); |
|
||||||
http.open("POST", '/api/alias', true); |
|
||||||
|
|
||||||
// Send the proper header information along with the request |
|
||||||
http.setRequestHeader("Content-type", "application/json"); |
|
||||||
|
|
||||||
http.onreadystatechange = function() { |
|
||||||
if (http.readyState == 4) { |
|
||||||
data = JSON.parse(http.responseText); |
|
||||||
if (http.status == 200) { |
|
||||||
aliasOK = true; |
|
||||||
$alias.removeClass('error'); |
|
||||||
$aliasSite.className = $aliasSite.className.replace(/(?:^|\s)demo(?!\S)/g, ''); |
|
||||||
$aliasSite.className = $aliasSite.className.replace(/(?:^|\s)error(?!\S)/g, ''); |
|
||||||
$aliasSite.innerHTML = '{{ if .Federation }}@<strong>' + data.data + '</strong>@{{.FriendlyHost}}{{ else }}{{.FriendlyHost}}/<strong>' + data.data + '</strong>/{{ end }}'; |
|
||||||
} else { |
|
||||||
aliasOK = false; |
|
||||||
$alias.setClass('error'); |
|
||||||
$aliasSite.className = 'error'; |
|
||||||
$aliasSite.textContent = data.error_msg; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
http.send(JSON.stringify(params)); |
|
||||||
} else { |
|
||||||
$aliasSite.className += ' demo'; |
|
||||||
$aliasSite.innerHTML = '{{ if .Federation }}@<strong>your-username</strong>@{{.FriendlyHost}}{{ else }}{{.FriendlyHost}}/<strong>your-username</strong>/{{ end }}'; |
|
||||||
} |
|
||||||
}; |
|
||||||
$alias.on('keyup input', function() { |
|
||||||
clearTimeout(typingTimer); |
|
||||||
typingTimer = setTimeout(doneTyping, doneTypingInterval); |
|
||||||
}); |
|
||||||
</script> |
</script> |
||||||
|
|
||||||
{{end}} |
{{end}} |
||||||
|
Loading…
Reference in new issue