Merge branch 'develop' into rename-account-suspend
This commit is contained in:
commit
f70c1dfaa5
|
@ -1,7 +1,7 @@
|
||||||
// +build !sqlite,!wflib
|
// +build !sqlite,!wflib
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright © 2019 A Bunch Tell LLC.
|
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -28,3 +28,15 @@ func (db *datastore) isDuplicateKeyErr(err error) bool {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *datastore) isIgnorableError(err error) bool {
|
||||||
|
if db.driverName == driverMySQL {
|
||||||
|
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||||
|
return mysqlErr.Number == mySQLErrCollationMix
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Error("isIgnorableError: failed check for unrecognized driver '%s'", db.driverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -48,3 +48,15 @@ func (db *datastore) isDuplicateKeyErr(err error) bool {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *datastore) isIgnorableError(err error) bool {
|
||||||
|
if db.driverName == driverMySQL {
|
||||||
|
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
|
||||||
|
return mysqlErr.Number == mySQLErrCollationMix
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Error("isIgnorableError: failed check for unrecognized driver '%s'", db.driverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018 A Bunch Tell LLC.
|
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -38,6 +38,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mySQLErrDuplicateKey = 1062
|
mySQLErrDuplicateKey = 1062
|
||||||
|
mySQLErrCollationMix = 1267
|
||||||
|
|
||||||
driverMySQL = "mysql"
|
driverMySQL = "mysql"
|
||||||
driverSQLite = "sqlite3"
|
driverSQLite = "sqlite3"
|
||||||
|
@ -2328,7 +2329,7 @@ func (db *datastore) GetUserInvite(id string) (*Invite, error) {
|
||||||
var i Invite
|
var i Invite
|
||||||
err := db.QueryRow("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE id = ?", id).Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive)
|
err := db.QueryRow("SELECT id, max_uses, created, expires, inactive FROM userinvites WHERE id = ?", id).Scan(&i.ID, &i.MaxUses, &i.Created, &i.Expires, &i.Inactive)
|
||||||
switch {
|
switch {
|
||||||
case err == sql.ErrNoRows:
|
case err == sql.ErrNoRows, db.isIgnorableError(err):
|
||||||
return nil, impart.HTTPError{http.StatusNotFound, "Invite doesn't exist."}
|
return nil, impart.HTTPError{http.StatusNotFound, "Invite doesn't exist."}
|
||||||
case err != nil:
|
case err != nil:
|
||||||
log.Error("Failed selecting invite: %v", err)
|
log.Error("Failed selecting invite: %v", err)
|
||||||
|
|
|
@ -57,11 +57,18 @@ func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Req
|
||||||
p := struct {
|
p := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
Invites *[]Invite
|
Invites *[]Invite
|
||||||
|
Suspended bool
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, "Invite People", f),
|
UserPage: NewUserPage(app, r, u, "Invite People", f),
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
p.Suspended, err = app.db.IsUserSuspended(u.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("view invites: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
p.Invites, err = app.db.GetUserInvites(u.ID)
|
p.Invites, err = app.db.GetUserInvites(u.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -164,7 +164,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||||
// Handle special pages first
|
// Handle special pages first
|
||||||
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))
|
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))
|
||||||
write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired))
|
write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired))
|
||||||
write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET")
|
write.HandleFunc("/invite/{code:[a-zA-Z0-9]+}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET")
|
||||||
// TODO: show a reader-specific 404 page if the function is disabled
|
// TODO: show a reader-specific 404 page if the function is disabled
|
||||||
write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader))
|
write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader))
|
||||||
RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter())
|
RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter())
|
||||||
|
@ -178,8 +178,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||||
}
|
}
|
||||||
|
|
||||||
// All the existing stuff
|
// All the existing stuff
|
||||||
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
|
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
|
||||||
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET")
|
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelUser)).Methods("GET")
|
||||||
// Collections
|
// Collections
|
||||||
if apper.App().cfg.App.SingleUser {
|
if apper.App().cfg.App.SingleUser {
|
||||||
RouteCollections(handler, write.PathPrefix("/").Subrouter())
|
RouteCollections(handler, write.PathPrefix("/").Subrouter())
|
||||||
|
|
|
@ -42,13 +42,16 @@
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<script>
|
<script>
|
||||||
|
// timezone offset in seconds
|
||||||
|
const tzOffsetSec = new Date().getTimezoneOffset() * 60;
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const fileDates = document.getElementById('fileDates');
|
const fileDates = document.getElementById('fileDates');
|
||||||
fileInput.addEventListener('change', (e) => {
|
fileInput.addEventListener('change', (e) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
let dateMap = {};
|
let dateMap = {};
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
dateMap[file.name] = file.lastModified / 1000;
|
// convert from milliseconds to seconds and adjust for tz
|
||||||
|
dateMap[file.name] = Math.round(file.lastModified / 1000) + tzOffsetSec;
|
||||||
}
|
}
|
||||||
fileDates.value = JSON.stringify(dateMap);
|
fileDates.value = JSON.stringify(dateMap);
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,6 +20,9 @@ table td {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="snug content-container">
|
<div class="snug content-container">
|
||||||
|
{{if .Suspended}}
|
||||||
|
{{template "user-suspended"}}
|
||||||
|
{{end}}
|
||||||
<h1>Invite people</h1>
|
<h1>Invite people</h1>
|
||||||
<p>Invite others to join <em>{{.SiteName}}</em> by generating and sharing invite links below.</p>
|
<p>Invite others to join <em>{{.SiteName}}</em> by generating and sharing invite links below.</p>
|
||||||
|
|
||||||
|
@ -27,7 +30,7 @@ table td {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="half">
|
<div class="half">
|
||||||
<label for="uses">Maximum number of uses:</label>
|
<label for="uses">Maximum number of uses:</label>
|
||||||
<select id="uses" name="uses">
|
<select id="uses" name="uses" {{if .Suspended}}disabled{{end}}>
|
||||||
<option value="0">No limit</option>
|
<option value="0">No limit</option>
|
||||||
<option value="1">1 use</option>
|
<option value="1">1 use</option>
|
||||||
<option value="5">5 uses</option>
|
<option value="5">5 uses</option>
|
||||||
|
@ -39,7 +42,7 @@ table td {
|
||||||
</div>
|
</div>
|
||||||
<div class="half">
|
<div class="half">
|
||||||
<label for="expires">Expire after:</label>
|
<label for="expires">Expire after:</label>
|
||||||
<select id="expires" name="expires">
|
<select id="expires" name="expires" {{if .Suspended}}disabled{{end}}>
|
||||||
<option value="0">Never</option>
|
<option value="0">Never</option>
|
||||||
<option value="30">30 minutes</option>
|
<option value="30">30 minutes</option>
|
||||||
<option value="60">1 hour</option>
|
<option value="60">1 hour</option>
|
||||||
|
@ -52,7 +55,7 @@ table td {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input type="submit" value="Generate" />
|
<input type="submit" value="Generate" {{if .Suspended}}disabled title="You cannot generate invites while your account is silenced."{{end}} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue