Page MenuHomeMusing Studio

No OneTemporary

diff --git a/author/author.go b/author/author.go
index bf3bfe1..0114905 100644
--- a/author/author.go
+++ b/author/author.go
@@ -1,127 +1,128 @@
/*
- * Copyright © 2018 A Bunch Tell LLC.
+ * Copyright © 2018-2020 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package author
import (
"github.com/writeas/writefreely/config"
"os"
"path/filepath"
"regexp"
)
// Regex pattern for valid usernames
var validUsernameReg = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-]*$")
// List of reserved usernames
var reservedUsernames = map[string]bool{
"a": true,
"about": true,
"add": true,
"admin": true,
"administrator": true,
"adminzone": true,
"api": true,
"article": true,
"articles": true,
"auth": true,
"authenticate": true,
"browse": true,
"c": true,
"categories": true,
"category": true,
"changes": true,
"community": true,
"create": true,
"css": true,
"data": true,
"dev": true,
"developers": true,
"draft": true,
"drafts": true,
"edit": true,
"edits": true,
"faq": true,
"feed": true,
"feedback": true,
"guide": true,
"guides": true,
"help": true,
"index": true,
"invite": true,
"js": true,
"login": true,
"logout": true,
"me": true,
"media": true,
"meta": true,
"metadata": true,
"new": true,
"news": true,
+ "oauth": true,
"post": true,
"posts": true,
"privacy": true,
"publication": true,
"publications": true,
"publish": true,
"random": true,
"read": true,
"reader": true,
"register": true,
"remove": true,
"signin": true,
"signout": true,
"signup": true,
"start": true,
"status": true,
"summary": true,
"support": true,
"tag": true,
"tags": true,
"team": true,
"template": true,
"templates": true,
"terms": true,
"terms-of-service": true,
"termsofservice": true,
"theme": true,
"themes": true,
"tips": true,
"tos": true,
"update": true,
"updates": true,
"user": true,
"users": true,
"yourname": true,
}
// IsValidUsername returns true if a given username is neither reserved nor
// of the correct format.
func IsValidUsername(cfg *config.Config, username string) bool {
// Username has to be above a character limit
if len(username) < cfg.App.MinUsernameLen {
return false
}
// Username is invalid if page with the same name exists. So traverse
// available pages, adding them to reservedUsernames map that'll be checked
// later.
filepath.Walk(filepath.Join(cfg.Server.PagesParentDir, "pages"), func(path string, i os.FileInfo, err error) error {
reservedUsernames[i.Name()] = true
return nil
})
// Username is invalid if it is reserved!
if _, reserved := reservedUsernames[username]; reserved {
return false
}
// TODO: use correct regexp function here
return len(validUsernameReg.FindStringSubmatch(username)) > 0
}
diff --git a/oauth_signup.go b/oauth_signup.go
index cf90af6..220afbd 100644
--- a/oauth_signup.go
+++ b/oauth_signup.go
@@ -1,206 +1,218 @@
+/*
+ * Copyright © 2020 A Bunch Tell LLC.
+ *
+ * This file is part of WriteFreely.
+ *
+ * WriteFreely is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, included
+ * in the LICENSE file in this source code package.
+ */
+
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
+ TokenAlias string // TODO: rename this to match the data it represents: the collection title
TokenEmail string
TokenRemoteUser string
Provider string
ClientID string
TokenHash string
- Username string
- Alias string
- Email string
+ LoginUsername string
+ Alias string // TODO: rename this to match the data it represents: the collection title
+ 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
+ TokenAlias string // TODO: rename this to match the data it represents: the collection title
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"))
+ var err error
+ hashedPass := []byte{}
+ clearPass := r.FormValue(oauthParamPassword)
+ hasPass := clearPass != ""
+ if hasPass {
+ hashedPass, err = auth.HashPass([]byte(clearPass))
+ 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,
+ HasPass: hasPass,
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) < h.Config.App.MinUsernameLen {
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too short."}
}
if len(username) > 100 {
return impart.HTTPError{Status: http.StatusBadRequest, Message: "Username is too long."}
}
- alias := r.FormValue(oauthParamAlias)
- if len(alias) == 0 {
- return impart.HTTPError{Status: http.StatusBadRequest, Message: "Alias is too short."}
- }
- password := r.FormValue("password")
- if len(password) == 0 {
- return impart.HTTPError{Status: http.StatusBadRequest, Message: "Password is too short."}
+ collTitle := r.FormValue(oauthParamAlias)
+ if len(collTitle) == 0 {
+ collTitle = username
}
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
+ collTitle := 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
+ collTitle = 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,
+ LoginUsername: username,
+ Alias: collTitle,
+ 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
}
diff --git a/oauth_slack.go b/oauth_slack.go
index 8cf4992..35db156 100644
--- a/oauth_slack.go
+++ b/oauth_slack.go
@@ -1,170 +1,180 @@
+/*
+ * Copyright © 2019-2020 A Bunch Tell LLC.
+ *
+ * This file is part of WriteFreely.
+ *
+ * WriteFreely is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, included
+ * in the LICENSE file in this source code package.
+ */
+
package writefreely
import (
"context"
"errors"
"fmt"
"github.com/writeas/nerds/store"
"github.com/writeas/slug"
"net/http"
"net/url"
"strings"
)
type slackOauthClient struct {
ClientID string
ClientSecret string
TeamID string
CallbackLocation string
HttpClient HttpClient
}
type slackExchangeResponse struct {
OK bool `json:"ok"`
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
TeamName string `json:"team_name"`
TeamID string `json:"team_id"`
Error string `json:"error"`
}
type slackIdentity struct {
Name string `json:"name"`
ID string `json:"id"`
Email string `json:"email"`
}
type slackTeam struct {
Name string `json:"name"`
ID string `json:"id"`
}
type slackUserIdentityResponse struct {
OK bool `json:"ok"`
User slackIdentity `json:"user"`
Team slackTeam `json:"team"`
Error string `json:"error"`
}
const (
slackAuthLocation = "https://slack.com/oauth/authorize"
slackExchangeLocation = "https://slack.com/api/oauth.access"
slackIdentityLocation = "https://slack.com/api/users.identity"
)
var _ oauthClient = slackOauthClient{}
func (c slackOauthClient) GetProvider() string {
return "slack"
}
func (c slackOauthClient) GetClientID() string {
return c.ClientID
}
func (c slackOauthClient) GetCallbackLocation() string {
return c.CallbackLocation
}
func (c slackOauthClient) buildLoginURL(state string) (string, error) {
u, err := url.Parse(slackAuthLocation)
if err != nil {
return "", err
}
q := u.Query()
q.Set("client_id", c.ClientID)
q.Set("scope", "identity.basic identity.email identity.team")
q.Set("redirect_uri", c.CallbackLocation)
q.Set("state", state)
// If this param is not set, the user can select which team they
// authenticate through and then we'd have to match the configured team
// against the profile get. That is extra work in the post-auth phase
// that we don't want to do.
q.Set("team", c.TeamID)
// The Slack OAuth docs don't explicitly list this one, but it is part of
// the spec, so we include it anyway.
q.Set("response_type", "code")
u.RawQuery = q.Encode()
return u.String(), nil
}
func (c slackOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) {
form := url.Values{}
// The oauth.access documentation doesn't explicitly mention this
// parameter, but it is part of the spec, so we include it anyway.
// https://api.slack.com/methods/oauth.access
form.Add("grant_type", "authorization_code")
form.Add("redirect_uri", c.CallbackLocation)
form.Add("code", code)
req, err := http.NewRequest("POST", slackExchangeLocation, strings.NewReader(form.Encode()))
if err != nil {
return nil, err
}
req.WithContext(ctx)
req.Header.Set("User-Agent", "writefreely")
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(c.ClientID, c.ClientSecret)
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New("unable to exchange code for access token")
}
var tokenResponse slackExchangeResponse
if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil {
return nil, err
}
if !tokenResponse.OK {
return nil, errors.New(tokenResponse.Error)
}
return tokenResponse.TokenResponse(), nil
}
func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) {
req, err := http.NewRequest("GET", slackIdentityLocation, nil)
if err != nil {
return nil, err
}
req.WithContext(ctx)
req.Header.Set("User-Agent", "writefreely")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+accessToken)
resp, err := c.HttpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, errors.New("unable to inspect access token")
}
var inspectResponse slackUserIdentityResponse
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
return nil, err
}
if !inspectResponse.OK {
return nil, errors.New(inspectResponse.Error)
}
return inspectResponse.InspectResponse(), nil
}
func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse {
return &InspectResponse{
UserID: resp.User.ID,
- Username: fmt.Sprintf("%s-%s", slug.Make(resp.User.Name), store.Generate62RandomString(5)),
+ Username: fmt.Sprintf("%s-%s", slug.Make(resp.User.Name), store.GenerateRandomString("0123456789bcdfghjklmnpqrstvwxyz", 5)),
DisplayName: resp.User.Name,
Email: resp.User.Email,
}
}
func (resp slackExchangeResponse) TokenResponse() *TokenResponse {
return &TokenResponse{
AccessToken: resp.AccessToken,
}
}
diff --git a/pages/signup-oauth.tmpl b/pages/signup-oauth.tmpl
index 34081cf..ecf5db0 100644
--- a/pages/signup-oauth.tmpl
+++ b/pages/signup-oauth.tmpl
@@ -1,118 +1,174 @@
{{define "head"}}<title>Log in &mdash; {{.SiteName}}</title>
<meta name="description" content="Log in to {{.SiteName}}.">
<meta itemprop="description" content="Log in to {{.SiteName}}.">
<style>input{margin-bottom:0.5em;}</style>
<style type="text/css">
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}}
{{define "content"}}
<div id="pricing" class="tight content-container">
<h1>Log in to {{.SiteName}}</h1>
{{if .Flashes}}<ul class="errors">
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}}
<div id="billing">
- <form action="/oauth/signup" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()">
+ <form action="/oauth/signup" method="post" style="text-align: center;margin-top:1em;" onsubmit="return disableSubmit()">
<input type="hidden" name="access_token" value="{{ .AccessToken }}" />
<input type="hidden" name="token_username" value="{{ .TokenUsername }}" />
<input type="hidden" name="token_alias" value="{{ .TokenAlias }}" />
<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 }}" />
<dl class="billing">
<label>
- <dt>Blog Title</dt>
+ <dt>Display Name</dt>
<dd>
- <input type="text" style="width: 100%; box-sizing: border-box;" name="alias" placeholder="Alias"{{ if .Alias }} value="{{.Alias}}"{{ end }} />
+ <input type="text" style="width: 100%; box-sizing: border-box;" name="alias" placeholder="Name"{{ if .Alias }} value="{{.Alias}}"{{ end }} />
</dd>
</label>
<label>
<dt>Username</dt>
<dd>
- <input type="text" name="username" style="width: 100%; box-sizing: border-box;" placeholder="Username" value="{{.Username}}" /><br />
+ <input type="text" id="username" name="username" style="width: 100%; box-sizing: border-box;" placeholder="Username" value="{{.LoginUsername}}" /><br />
{{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</dt>
<dd>
<input type="text" name="email" style="width: 100%; box-sizing: border-box;" placeholder="Email"{{ if .Email }} value="{{.Email}}"{{ end }} />
</dd>
</label>
- <label>
- <dt>Password</dt>
- <dd>
-<input type="password" name="password" style="width: 100%; box-sizing: border-box;" placeholder="Password" /><br />
- </dd>
- </label>
<dt>
<input type="submit" id="btn-login" value="Login" />
</dt>
</dl>
</form>
</div>
+<script type="text/javascript" src="/js/h.js"></script>
<script type="text/javascript">
+// Copied from signup.tmpl
+// NOTE: this element is named "alias" on signup.tmpl and "username" here
+var $alias = H.getEl('username');
+
function disableSubmit() {
+ // Validate input
+ if (!aliasOK) {
+ var $a = $alias;
+ $a.el.className = 'error';
+ $a.el.focus();
+ $a.el.scrollIntoView();
+ return false;
+ }
+
var $btn = document.getElementById("btn-login");
$btn.value = "Logging in...";
$btn.disabled = true;
+ return true;
}
+
+// Copied from signup.tmpl
+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);
+});
+doneTyping();
</script>
{{end}}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Nov 6, 4:50 AM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3474162

Event Timeline