* Add the Age Gate middleware for NSFW sites. * Cache thumbnail images from blog entries. * Implement the user-root properly for loading web assets.master
parent
c1995efb7a
commit
383b5d7591
14 changed files with 433 additions and 31 deletions
@ -1,5 +1,4 @@ |
||||
bin/ |
||||
pvt-www/static/photos/ |
||||
public_html/ |
||||
pkg/bundled/ |
||||
public_html/.settings.json |
||||
*.sqlite |
||||
|
@ -0,0 +1,209 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"os" |
||||
"path/filepath" |
||||
"sort" |
||||
|
||||
"git.kirsle.net/apps/gophertype/pkg" |
||||
"git.kirsle.net/apps/gophertype/pkg/console" |
||||
_ "git.kirsle.net/apps/gophertype/pkg/controllers" |
||||
"git.kirsle.net/apps/gophertype/pkg/models" |
||||
_ "github.com/jinzhu/gorm/dialects/mysql" |
||||
_ "github.com/jinzhu/gorm/dialects/postgres" |
||||
_ "github.com/jinzhu/gorm/dialects/sqlite" |
||||
jsondb "github.com/kirsle/blog/jsondb" |
||||
mComments "github.com/kirsle/blog/models/comments" |
||||
mPosts "github.com/kirsle/blog/models/posts" |
||||
) |
||||
|
||||
// Command-line flags.
|
||||
var ( |
||||
optSourceRoot string |
||||
optRoot string |
||||
|
||||
// Database option flags.
|
||||
optSQLite string |
||||
optPostgres string |
||||
optMySQL string |
||||
|
||||
// Chosen DB options.
|
||||
dbDriver string |
||||
dbPath string |
||||
) |
||||
|
||||
// Other global variables
|
||||
var ( |
||||
JsonDB *jsondb.DB |
||||
) |
||||
|
||||
func init() { |
||||
flag.StringVar(&optSourceRoot, "srcroot", "", "User root from old kirsle/blog website") |
||||
flag.StringVar(&optRoot, "root", "", "User root for GopherType") |
||||
|
||||
// Database driver. Choose one.
|
||||
flag.StringVar(&optSQLite, "sqlite3", "", "Use SQLite database, default 'database.sqlite'") |
||||
flag.StringVar(&optPostgres, "postgres", "", |
||||
"Use Postgres database, format: "+ |
||||
"host=myhost port=myport user=gorm dbname=gorm password=mypassword") |
||||
flag.StringVar(&optMySQL, "mysql", "", |
||||
"Use MySQL database, format: "+ |
||||
"user:password@/dbname?charset=utf8&parseTime=True&loc=Local") |
||||
} |
||||
|
||||
func main() { |
||||
console.SetDebug(false) |
||||
flag.Parse() |
||||
|
||||
// Validate the choice of database.
|
||||
if optSQLite != "" { |
||||
dbDriver = "sqlite3" |
||||
dbPath = optSQLite |
||||
} else if optPostgres != "" { |
||||
dbDriver = "postgres" |
||||
dbPath = optPostgres |
||||
} else if optMySQL != "" { |
||||
dbDriver = "mysql" |
||||
dbPath = optMySQL |
||||
} else { |
||||
fmt.Print( |
||||
"Specify a DB driver for Gophertype, similar to the gophertype command.", |
||||
) |
||||
os.Exit(1) |
||||
} |
||||
|
||||
// Validate the roots are given.
|
||||
if optSourceRoot == "" { |
||||
fmt.Print( |
||||
"Missing -srcroot parameter: this should be the User Root from the legacy kirsle/blog site.", |
||||
) |
||||
os.Exit(1) |
||||
} |
||||
if optRoot == "" { |
||||
fmt.Print( |
||||
"Missing required -root parameter: this is the User Root for Gophertype", |
||||
) |
||||
} |
||||
|
||||
// Initialize the old JsonDB.
|
||||
|
||||
app := gophertype.NewSite(optRoot) |
||||
app.UseDB(dbDriver, dbPath) |
||||
|
||||
initJsonDB() |
||||
doMigrate() |
||||
} |
||||
|
||||
// Initialize the old kirsle/blog JsonDB.
|
||||
func initJsonDB() { |
||||
JsonDB = jsondb.New(filepath.Join(optSourceRoot, ".private")) |
||||
mPosts.DB = JsonDB |
||||
mComments.DB = JsonDB |
||||
} |
||||
|
||||
// doMigrate is the head of the migrate functions.
|
||||
func doMigrate() { |
||||
migratePosts() |
||||
migrateComments() |
||||
} |
||||
|
||||
// migratePosts migrates blog posts over.
|
||||
func migratePosts() { |
||||
console.Warn("BEGIN: Migrating blog posts") |
||||
idx, err := mPosts.GetIndex() |
||||
if err != nil { |
||||
panic("migratePosts: GetIndex: " + err.Error()) |
||||
} |
||||
|
||||
// Sort the IDs to make it pretty.
|
||||
var sortedIds = []int{} |
||||
for id := range idx.Posts { |
||||
sortedIds = append(sortedIds, id) |
||||
} |
||||
sort.Ints(sortedIds) |
||||
|
||||
for _, id := range sortedIds { |
||||
post, err := mPosts.Load(id) |
||||
if err != nil { |
||||
panic(fmt.Sprintf("migratePosts: error loading legacy post ID %d: %s", id, err)) |
||||
} |
||||
|
||||
console.Info("Post ID %d: %s", id, post.Title) |
||||
|
||||
// Create the post in Gophertype.
|
||||
p := models.Post{ |
||||
Title: post.Title, |
||||
Fragment: post.Fragment, |
||||
ContentType: post.ContentType, |
||||
AuthorID: uint(post.AuthorID), |
||||
Body: post.Body, |
||||
Privacy: post.Privacy, |
||||
Sticky: post.Sticky, |
||||
EnableComments: post.EnableComments, |
||||
} |
||||
p.ID = uint(post.ID) |
||||
p.CreatedAt = post.Created |
||||
p.UpdatedAt = post.Updated |
||||
|
||||
// Convert tags.
|
||||
for _, tag := range post.Tags { |
||||
p.Tags = append(p.Tags, models.TaggedPost{ |
||||
Tag: tag, |
||||
}) |
||||
} |
||||
|
||||
err = p.Save() |
||||
if err != nil { |
||||
console.Error("Error saving post %d: %s", p.ID, err) |
||||
} |
||||
} |
||||
|
||||
console.Warn("FINISH: Migrating blog posts") |
||||
} |
||||
|
||||
// migrateComments migrates comments over.
|
||||
func migrateComments() { |
||||
console.Warn("BEGIN: Migrating comments") |
||||
|
||||
// Find all the comment threads.
|
||||
files, err := JsonDB.List("comments/threads") |
||||
if err != nil { |
||||
panic("Error listing comments: " + err.Error()) |
||||
} |
||||
for _, file := range files { |
||||
document := filepath.Base(file) |
||||
thread, err := mComments.Load(document) |
||||
if err != nil { |
||||
panic(fmt.Sprintf("Error loading comment thread %s: %s", file, err)) |
||||
} |
||||
|
||||
for _, comment := range thread.Comments { |
||||
// Migrate to the new model.
|
||||
com := models.Comment{ |
||||
Thread: thread.ID, |
||||
UserID: uint(comment.UserID), |
||||
Name: comment.Name, |
||||
Email: comment.Email, |
||||
Avatar: comment.Avatar, |
||||
Body: comment.Body, |
||||
EditToken: comment.EditToken, |
||||
DeleteToken: comment.DeleteToken, |
||||
} |
||||
com.CreatedAt = comment.Created |
||||
com.UpdatedAt = comment.Updated |
||||
|
||||
// Special case for guestbook page.
|
||||
if thread.ID == "guestbook" { |
||||
com.OriginURL = "/guestbook" |
||||
} |
||||
|
||||
if err := com.Save(); err != nil { |
||||
console.Error("Error saving comment: %s", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
console.Warn("FINISH: Migrating comments") |
||||
} |
@ -0,0 +1,43 @@ |
||||
package controllers |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"git.kirsle.net/apps/gophertype/pkg/glue" |
||||
"git.kirsle.net/apps/gophertype/pkg/responses" |
||||
"git.kirsle.net/apps/gophertype/pkg/session" |
||||
) |
||||
|
||||
func init() { |
||||
glue.Register(glue.Endpoint{ |
||||
Path: "/age-verify", |
||||
Methods: []string{"GET", "POST"}, |
||||
Handler: AgeVerify, |
||||
}) |
||||
} |
||||
|
||||
// AgeVerify handles the age gate prompt page for NSFW sites.
|
||||
func AgeVerify(w http.ResponseWriter, r *http.Request) { |
||||
var ( |
||||
v = responses.NewTemplateVars(w, r) |
||||
next = r.FormValue("next") |
||||
confirm = r.FormValue("confirm") |
||||
) |
||||
|
||||
if next == "" { |
||||
next = "/" |
||||
} |
||||
v.V["Next"] = next |
||||
|
||||
if r.Method == http.MethodPost { |
||||
if confirm == "true" { |
||||
session := session.Get(r) |
||||
session.Values["age-ok"] = true |
||||
session.Save(r, w) |
||||
responses.Redirect(w, r, next) |
||||
return |
||||
} |
||||
} |
||||
|
||||
responses.RenderTemplate(w, r, "_builtin/age-gate.gohtml", v) |
||||
} |
@ -0,0 +1,80 @@ |
||||
package middleware |
||||
|
||||
import ( |
||||
"net/http" |
||||
"strings" |
||||
|
||||
"git.kirsle.net/apps/gophertype/pkg/responses" |
||||
"git.kirsle.net/apps/gophertype/pkg/session" |
||||
"git.kirsle.net/apps/gophertype/pkg/settings" |
||||
) |
||||
|
||||
// URL suffixes to allow to bypass the age gate middleware.
|
||||
var ageGateSuffixes = []string{ |
||||
"/blog.rss", // Allow public access to RSS and Atom feeds.
|
||||
"/blog.atom", |
||||
"/blog.json", |
||||
".js", |
||||
".css", |
||||
".txt", |
||||
".ico", |
||||
".png", |
||||
".jpg", |
||||
".jpeg", |
||||
".gif", |
||||
".mp4", |
||||
".webm", |
||||
".ttf", |
||||
".eot", |
||||
".svg", |
||||
".woff", |
||||
".woff2", |
||||
} |
||||
|
||||
// AgeGate is a middleware generator that does age verification for NSFW sites.
|
||||
// Single GET requests with ?over18=1 parameter may skip the middleware check.
|
||||
func AgeGate(next http.Handler) http.Handler { |
||||
middleware := func(w http.ResponseWriter, r *http.Request) { |
||||
s := settings.Current |
||||
if !s.NSFW { |
||||
next.ServeHTTP(w, r) |
||||
return |
||||
} |
||||
|
||||
path := r.URL.Path |
||||
|
||||
// Let the age-verify handler catch its route.
|
||||
if strings.HasPrefix(path, "/age-verify") { |
||||
next.ServeHTTP(w, r) |
||||
return |
||||
} |
||||
|
||||
// Allow static file requests to skip the check.
|
||||
for _, suffix := range ageGateSuffixes { |
||||
if strings.HasSuffix(path, suffix) { |
||||
next.ServeHTTP(w, r) |
||||
return |
||||
} |
||||
} |
||||
|
||||
// POST requests are permitted (e.g. post a comment on a /?over18=1 page)
|
||||
if r.Method == http.MethodPost { |
||||
next.ServeHTTP(w, r) |
||||
return |
||||
} |
||||
|
||||
// Finally, check if they've confirmed their age on the age-verify handler.
|
||||
ses := session.Get(r) |
||||
if val, _ := ses.Values["age-ok"].(bool); !val { |
||||
// They haven't been verified. Redirect them to the age-verify handler.
|
||||
if r.FormValue("over18") == "" { |
||||
responses.Redirect(w, r, "/age-verify?next="+path) |
||||
return |
||||
} |
||||
} |
||||
|
||||
next.ServeHTTP(w, r) |
||||
} |
||||
|
||||
return http.HandlerFunc(middleware) |
||||
} |
@ -0,0 +1,34 @@ |
||||
{{ define "title" }}Age Verification{{ end }} |
||||
{{ define "content" }} |
||||
<div class="card"> |
||||
<div class="card-body"> |
||||
<form action="/age-verify" method="POST"> |
||||
{{ CSRF }} |
||||
<input type="hidden" name="next" value="{{ .V.Next }}"> |
||||
<input type="hidden" name="confirm" value="true"> |
||||
|
||||
<h1>Restricted Content</h1> |
||||
|
||||
<p> |
||||
This website has been marked <abbr title="Not Safe For Work">NSFW</abbr> |
||||
by its owner. It may contain nudity or content not suited for users |
||||
under the age of 18. |
||||
</p> |
||||
|
||||
<p> |
||||
To proceed, you must verify you are at least 18 years or older. |
||||
</p> |
||||
|
||||
<button type="submit" |
||||
class="btn btn-danger"> |
||||
I am 18 years or older |
||||
</button> |
||||
<a class="btn btn-primary" |
||||
href="https://duckduckgo.com/"> |
||||
Get me out of here! |
||||
</a> |
||||
|
||||
</form> |
||||
</div> |
||||
</div> |
||||
{{ end }} |
Loading…
Reference in new issue