Maintenance Mode + Blockable Admin Scope
This commit is contained in:
parent
5f5fe37350
commit
bf71ed421c
|
@ -23,8 +23,10 @@ const (
|
|||
// Website administration
|
||||
// - Forum: ability to manage available forums
|
||||
// - Scopes: ability to manage admin groups & scopes
|
||||
// - Maintenance mode
|
||||
ScopeForumAdmin = "admin.forum.manage"
|
||||
ScopeAdminScopeAdmin = "admin.scope.manage"
|
||||
ScopeMaintenance = "admin.maintenance"
|
||||
|
||||
// User account admin
|
||||
// - Impersonate: ability to log in as a user account
|
||||
|
@ -35,6 +37,9 @@ const (
|
|||
ScopeUserPromote = "admin.user.promote"
|
||||
ScopeUserDelete = "admin.user.delete"
|
||||
|
||||
// Admins with this scope can not be blocked by users.
|
||||
ScopeUnblockable = "admin.unblockable"
|
||||
|
||||
// Special scope to mark an admin automagically in the Inner Circle
|
||||
ScopeIsInnerCircle = "admin.override.inner-circle"
|
||||
)
|
||||
|
@ -61,6 +66,7 @@ func ListAdminScopes() []string {
|
|||
ScopeUserBan,
|
||||
ScopeUserDelete,
|
||||
ScopeUserPromote,
|
||||
ScopeUnblockable,
|
||||
ScopeIsInnerCircle,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,16 @@ import (
|
|||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Version of the config format - when new fields are added, it will attempt
|
||||
// to write the settings.toml to disk so new defaults populate.
|
||||
var currentVersion = 1
|
||||
|
||||
// Current loaded settings.json
|
||||
var Current = DefaultVariable()
|
||||
|
||||
// Variable configuration attributes (loaded from settings.json).
|
||||
type Variable struct {
|
||||
Version int
|
||||
BaseURL string
|
||||
AdminEmail string
|
||||
CronAPIKey string
|
||||
|
@ -23,6 +28,7 @@ type Variable struct {
|
|||
Redis Redis
|
||||
Database Database
|
||||
BareRTC BareRTC
|
||||
Maintenance Maintenance
|
||||
UseXForwardedFor bool
|
||||
}
|
||||
|
||||
|
@ -65,15 +71,7 @@ func LoadSettings() {
|
|||
|
||||
Current = v
|
||||
} else {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetIndent("", " ")
|
||||
err := enc.Encode(DefaultVariable())
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("LoadSettings: couldn't marshal default settings: %s", err))
|
||||
}
|
||||
|
||||
ioutil.WriteFile(SettingsPath, buf.Bytes(), 0600)
|
||||
WriteSettings()
|
||||
log.Warn("NOTICE: Created default settings.json file - review it and configure mail servers and database!")
|
||||
}
|
||||
|
||||
|
@ -82,6 +80,30 @@ func LoadSettings() {
|
|||
log.Error("No database configured in settings.json. Choose SQLite or Postgres and update the DB connector string!")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Have we added new config fields? Save the settings.json.
|
||||
if Current.Version != currentVersion {
|
||||
log.Warn("New options are available for your settings.json file. Your settings will be re-saved now.")
|
||||
Current.Version = currentVersion
|
||||
if err := WriteSettings(); err != nil {
|
||||
log.Error("Couldn't write your settings.json file: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSettings will commit the settings.json to disk.
|
||||
func WriteSettings() error {
|
||||
log.Error("Note: initial settings.json was written to disk.")
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetIndent("", " ")
|
||||
err := enc.Encode(Current)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("WriteSettings: couldn't marshal settings: %s", err))
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(SettingsPath, buf.Bytes(), 0600)
|
||||
}
|
||||
|
||||
// Mail settings.
|
||||
|
@ -114,3 +136,11 @@ type BareRTC struct {
|
|||
JWTSecret string
|
||||
URL string
|
||||
}
|
||||
|
||||
// Maintenance mode settings.
|
||||
type Maintenance struct {
|
||||
PauseSignup bool
|
||||
PauseLogin bool
|
||||
PauseChat bool
|
||||
PauseInteraction bool
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/ratelimit"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
|
@ -70,6 +71,11 @@ func Login() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Maintenance mode check.
|
||||
if middleware.LoginMaintenance(user, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// OK. Log in the user's session.
|
||||
session.LoginUser(w, r, user)
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
|
@ -51,6 +52,11 @@ func Profile() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Is the site under a Maintenance Mode restriction?
|
||||
if middleware.MaintenanceMode(currentUser, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Forcing an external view? (preview of logged-out profile view for visibility=external accounts)
|
||||
// You must be logged-in actually to see this.
|
||||
if r.FormValue("view") == "external" {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"code.nonshy.com/nonshy/website/pkg/mail"
|
||||
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/redis"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
|
@ -35,6 +36,11 @@ func Signup() http.HandlerFunc {
|
|||
tmpl := templates.Must("account/signup.html")
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Maintenance mode?
|
||||
if middleware.SignupMaintenance(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Template vars.
|
||||
var vars = map[string]interface{}{
|
||||
"SignupToken": "", // non-empty if user has clicked verification link
|
||||
|
|
73
pkg/controller/admin/maintenance.go
Normal file
73
pkg/controller/admin/maintenance.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
// Maintenance controller (/admin/maintenance)
|
||||
func Maintenance() http.HandlerFunc {
|
||||
tmpl := templates.Must("admin/maintenance.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Query parameters.
|
||||
var (
|
||||
intent = r.FormValue("intent")
|
||||
)
|
||||
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get your current user: %s", err)
|
||||
}
|
||||
|
||||
_ = currentUser
|
||||
|
||||
// POST event handlers.
|
||||
if r.Method == http.MethodPost {
|
||||
// Collect the form parameters.
|
||||
var (
|
||||
pauseSignup = r.PostFormValue("signup") == "true"
|
||||
pauseLogin = r.PostFormValue("login") == "true"
|
||||
pauseChat = r.PostFormValue("chat") == "true"
|
||||
pauseInteraction = r.PostFormValue("interaction") == "true"
|
||||
)
|
||||
|
||||
switch intent {
|
||||
case "everything", "nothing":
|
||||
pauseSignup = intent == "everything"
|
||||
pauseLogin = pauseSignup
|
||||
pauseChat = pauseSignup
|
||||
pauseInteraction = pauseSignup
|
||||
intent = "save"
|
||||
fallthrough
|
||||
case "save":
|
||||
// Update and save the site settings.
|
||||
config.Current.Maintenance.PauseSignup = pauseSignup
|
||||
config.Current.Maintenance.PauseLogin = pauseLogin
|
||||
config.Current.Maintenance.PauseChat = pauseChat
|
||||
config.Current.Maintenance.PauseInteraction = pauseInteraction
|
||||
if err := config.WriteSettings(); err != nil {
|
||||
session.FlashError(w, r, "Couldn't write settings.json: %s", err)
|
||||
} else {
|
||||
session.Flash(w, r, "Maintenance settings updated!")
|
||||
}
|
||||
default:
|
||||
session.FlashError(w, r, "Unsupported intent: %s", intent)
|
||||
}
|
||||
|
||||
templates.Redirect(w, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Intent": intent,
|
||||
"Maint": config.Current.Maintenance,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
|
@ -108,8 +108,8 @@ func BlockUser() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Can't block admins.
|
||||
if user.IsAdmin {
|
||||
// Can't block admins who have the unblockable scope.
|
||||
if user.IsAdmin && user.HasAdminScope(config.ScopeUnblockable) {
|
||||
// For curiosity's sake, log a report.
|
||||
fb := &models.Feedback{
|
||||
Intent: "report",
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"code.nonshy.com/nonshy/website/pkg/geoip"
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/photo"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
|
@ -67,6 +68,11 @@ func Landing() http.HandlerFunc {
|
|||
isShy = currentUser.IsShy()
|
||||
)
|
||||
if intent == "join" {
|
||||
// Maintenance mode?
|
||||
if middleware.ChatMaintenance(currentUser, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we are shy, block chat for now.
|
||||
if isShy {
|
||||
session.FlashError(w, r,
|
||||
|
|
|
@ -40,6 +40,11 @@ func LoginRequired(handler http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
// Is the site under a Maintenance Mode restriction?
|
||||
if MaintenanceMode(user, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ping LastLoginAt for long lived sessions, but not if impersonated.
|
||||
if time.Since(user.LastLoginAt) > config.LastLoginAtCooldown && !session.Impersonated(r) {
|
||||
user.LastLoginAt = time.Now()
|
||||
|
@ -123,6 +128,11 @@ func CertRequired(handler http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
// Is the site under a Maintenance Mode restriction?
|
||||
if MaintenanceMode(currentUser, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// User must be certified.
|
||||
if !currentUser.Certified || currentUser.ProfilePhoto.ID == 0 {
|
||||
log.Error("CertRequired: user is not certified")
|
||||
|
|
71
pkg/middleware/maintenance.go
Normal file
71
pkg/middleware/maintenance.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
var tmplMaint = templates.Must("errors/maintenance.html")
|
||||
|
||||
// MaintenanceMode check at the middleware level, e.g. to block
|
||||
// LoginRequired and CertificationRequired if site-wide interaction
|
||||
// is currently on hold. Returns true if handled.
|
||||
func MaintenanceMode(currentUser *models.User, w http.ResponseWriter, r *http.Request) bool {
|
||||
// Is the site under a Maintenance Mode restriction?
|
||||
if config.Current.Maintenance.PauseInteraction && !currentUser.IsAdmin {
|
||||
var vars = map[string]interface{}{
|
||||
"Reason": "interaction",
|
||||
}
|
||||
if err := tmplMaint.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SignupMaintenance may handle maintenance mode requests for signup gating.
|
||||
func SignupMaintenance(w http.ResponseWriter, r *http.Request) bool {
|
||||
if config.Current.Maintenance.PauseSignup {
|
||||
var vars = map[string]interface{}{
|
||||
"Reason": "signup",
|
||||
}
|
||||
if err := tmplMaint.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LoginMaintenance may handle maintenance mode requests for login gating.
|
||||
func LoginMaintenance(currentUser *models.User, w http.ResponseWriter, r *http.Request) bool {
|
||||
if config.Current.Maintenance.PauseLogin && !currentUser.IsAdmin {
|
||||
var vars = map[string]interface{}{
|
||||
"Reason": "login",
|
||||
}
|
||||
if err := tmplMaint.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ChatMaintenance may handle maintenance mode requests for chat room gating.
|
||||
func ChatMaintenance(currentUser *models.User, w http.ResponseWriter, r *http.Request) bool {
|
||||
if config.Current.Maintenance.PauseChat && !currentUser.IsAdmin {
|
||||
var vars = map[string]interface{}{
|
||||
"Reason": "chat",
|
||||
}
|
||||
if err := tmplMaint.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -87,6 +87,7 @@ func New() http.Handler {
|
|||
mux.Handle("/admin/photo/certification", middleware.AdminRequired("", photo.AdminCertification()))
|
||||
mux.Handle("/admin/feedback", middleware.AdminRequired("", admin.Feedback()))
|
||||
mux.Handle("/admin/user-action", middleware.AdminRequired("", admin.UserActions()))
|
||||
mux.Handle("/admin/maintenance", middleware.AdminRequired(config.ScopeMaintenance, admin.Maintenance()))
|
||||
mux.Handle("/forum/admin", middleware.AdminRequired(config.ScopeForumAdmin, forum.Manage()))
|
||||
mux.Handle("/forum/admin/edit", middleware.AdminRequired(config.ScopeForumAdmin, forum.AddEdit()))
|
||||
mux.Handle("/inner-circle/remove", middleware.AdminRequired(config.ScopeCircleModerator, account.RemoveCircle()))
|
||||
|
|
|
@ -148,6 +148,12 @@
|
|||
Admin Permissions Management
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/admin/maintenance">
|
||||
<i class="fa fa-wrench mr-2"></i>
|
||||
Maintenance Mode
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
108
web/templates/admin/maintenance.html
Normal file
108
web/templates/admin/maintenance.html
Normal file
|
@ -0,0 +1,108 @@
|
|||
{{define "title"}}Admin - Maintenance Mode{{end}}
|
||||
{{define "content"}}
|
||||
{{$Root := .}}
|
||||
<div class="container">
|
||||
<section class="hero is-danger is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
Maintenance Mode
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form method="POST" action="{{.Request.URL.Path}}">
|
||||
{{InputCSRF}}
|
||||
<div class="p-4">
|
||||
|
||||
<p class="block">
|
||||
This page allows you to set various Maintenance Mode flags that can pause or
|
||||
disable website features if needed.
|
||||
In an emergency, click on the Lock Down Everything button that will enable ALL maintenance
|
||||
mode flags and basically disable the whole website for everybody except admin user accounts.
|
||||
</p>
|
||||
|
||||
<p class="block">
|
||||
<button type="submit" class="button is-danger mr-2"
|
||||
name="intent" value="everything"
|
||||
onclick="return confirm('Do you want to lock down EVERYTHING?')">
|
||||
<i class="fa fa-exclamation-triangle mr-2"></i> Lock Down Everything
|
||||
</button>
|
||||
|
||||
<button type="submit" class="button is-success"
|
||||
name="intent" value="nothing"
|
||||
onclick="return confirm('Do you want to RESTORE ALL site functionality?')">
|
||||
<i class="fa fa-exclamation-triangle mr-2"></i> Restore Everything
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<label class="label">Maintenance Mode Settings</label>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
name="signup"
|
||||
value="true"
|
||||
{{if .Maint.PauseSignup}}checked{{end}}>
|
||||
Pause new account signups
|
||||
</label>
|
||||
<p class="help">
|
||||
New account signups are paused and a maintenance page is shown in its place.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
name="login"
|
||||
value="true"
|
||||
{{if .Maint.PauseLogin}}checked{{end}}>
|
||||
Pause new logins
|
||||
</label>
|
||||
<p class="help">
|
||||
The login page is disabled (except for admin user login). Already logged-in
|
||||
users can remain logged in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
name="chat"
|
||||
value="true"
|
||||
{{if .Maint.PauseChat}}checked{{end}}>
|
||||
Pause chat room entry
|
||||
</label>
|
||||
<p class="help">
|
||||
No new entries into the chat room are allowed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
name="interaction"
|
||||
value="true"
|
||||
{{if .Maint.PauseInteraction}}checked{{end}}>
|
||||
Pause <strong>all</strong> interactions
|
||||
</label>
|
||||
<p class="help">
|
||||
Every site feature becomes basically 'admin required' and users are given an error
|
||||
page where their only option is to log out or come back later.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<button type="submit" class="button is-primary"
|
||||
name="intent" value="save">
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
61
web/templates/errors/maintenance.html
Normal file
61
web/templates/errors/maintenance.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
{{define "title"}}Not Available{{end}}
|
||||
{{define "content"}}
|
||||
<div class="container">
|
||||
<section class="hero block is-warning is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">Not Available</h1>
|
||||
<h2 class="subtitle">The website is currently unavailable</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="block content p-4 mb-0">
|
||||
<h1>{{PrettyTitle}} is currently not available</h1>
|
||||
<p>
|
||||
We regret to inform you that {{PrettyTitle}} is currently not available.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The website is currently in "maintenance mode" and the feature you requested
|
||||
is currently on pause. Please check back again later.
|
||||
</p>
|
||||
|
||||
<!-- More information? -->
|
||||
{{if .Reason}}
|
||||
<h2>More Information</h2>
|
||||
{{end}}
|
||||
|
||||
{{if eq .Reason "interaction"}}
|
||||
<p>
|
||||
All user interaction on the website is currently on pause. You are currently
|
||||
logged in to an account (username: {{.CurrentUser.Username}}) and you may
|
||||
remain logged-in if you want, but all actions that require a logged-in account
|
||||
are currently disabled.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you'd like, you may <a href="/logout">log out</a> or just come back later
|
||||
and see if the maintenance mode of the website has been lifted.
|
||||
</p>
|
||||
{{else if eq .Reason "signup"}}
|
||||
<p>
|
||||
All new account signups are currently on pause. We are not accepting any new
|
||||
members at this time. Please check back again later.
|
||||
</p>
|
||||
{{else if eq .Reason "login"}}
|
||||
<p>
|
||||
All new account logins are currently on pause. Please try again later.
|
||||
</p>
|
||||
{{else if eq .Reason "chat"}}
|
||||
<p>
|
||||
The chat room is currently offline for maintenance. Please try again some other time.
|
||||
</p>
|
||||
{{else}}
|
||||
<p>
|
||||
No further information is available at this time.
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
Loading…
Reference in New Issue
Block a user