Browse Source

Completed template system, added login/logout

master
Gisle Aune 7 years ago
parent
commit
2d8bdf9643
  1. 2
      aitestory.json
  2. 59
      controllers/listcontroller.go
  3. 65
      controllers/usercontroller.go
  4. 13
      controllers/wikiauth.go
  5. BIN
      debug
  6. 1
      main.go
  7. 39
      model/category.go
  8. 94
      model/header.go
  9. 16
      model/page.go
  10. 4
      model/page_test.go
  11. 7
      model/tag.go
  12. 24
      model/tag_test.go
  13. 20
      view/renderer.go
  14. 9
      view/templates/base/default.tmpl
  15. 26
      view/templates/index.tmpl
  16. 21
      view/templates/login.tmpl
  17. 22
      viewmodel/base.go
  18. 9
      viewmodel/pagelist.go
  19. 19
      viewmodel/userlogin.go

2
aitestory.json

@ -16,7 +16,7 @@
},
"wiki": {
"url": "https://wiki.aiterp.net",
"url": "https://wiki.aiterp.net/w/api.php",
"username": "AiteBot@AiteStory",
"password": ""
},

59
controllers/listcontroller.go

@ -1,9 +1,13 @@
package controllers
import (
"fmt"
"net/http"
"strings"
"git.aiterp.net/gisle/wrouter/response"
"git.aiterp.net/AiteRP/aitestory/model"
"git.aiterp.net/AiteRP/aitestory/view"
"git.aiterp.net/AiteRP/aitestory/viewmodel"
@ -15,18 +19,71 @@ import (
var ListController = wrouter.Router{}
func listIndex(path string, w http.ResponseWriter, req *http.Request, user *auth.User) bool {
if req.Method != "GET" || strings.LastIndex(req.URL.Path, "/") >= len(path) {
var err error
if req.Method != "GET" || len(req.URL.Path) > len(path) {
return false
}
vm := viewmodel.PageList{}
vm.Headers, err = model.ListHeaders()
vm.Categories = model.PageCategories
vm.Setup(user)
if err != nil {
response.Text(w, 500, err.Error())
return true
}
view.Render(w, "index", 200, vm)
return true
}
func listFiltered(category model.PageCategory) wrouter.FunctionHandlerFunc {
return func(path string, w http.ResponseWriter, req *http.Request, user *auth.User) bool {
var err error
if req.Method != "GET" || strings.LastIndex(req.URL.Path, "/") >= len(path) {
return false
}
tagName := strings.Replace(req.URL.Path[len(path):], "_", " ", -1)
vm := viewmodel.PageList{}
if tagName != "" {
tag, err := model.FindTag("name", tagName)
if err != nil {
response.Text(w, 404, err.Error())
return true
}
vm.Headers, err = model.ListHeadersByTag(category.Key, tag)
} else {
vm.Headers, err = model.ListHeadersByCategory(category.Key)
}
vm.Categories = model.PageCategories
vm.ActiveCategory = category
vm.Setup(user)
if err != nil {
response.Text(w, 500, err.Error())
return true
}
view.Render(w, "index", 200, vm)
return true
}
}
// story.aiterp.net/Ruins_of_Rakhana
func init() {
ListController.Function("/", listIndex)
ListController.Function("/", listFiltered(model.PageCategory{Key: ""}))
for _, category := range model.PageCategories {
ListController.Function(fmt.Sprintf("/%s/", strings.ToLower(category.Plural)), listFiltered(category))
}
}

65
controllers/usercontroller.go

@ -0,0 +1,65 @@
package controllers
import (
"net/http"
"git.aiterp.net/AiteRP/aitestory/view"
"git.aiterp.net/AiteRP/aitestory/viewmodel"
"git.aiterp.net/gisle/wrouter"
"git.aiterp.net/gisle/wrouter/auth"
)
// UserController serves and handles the login form
var UserController = wrouter.Router{}
func userLogin(path string, w http.ResponseWriter, req *http.Request, user *auth.User) bool {
//var err error
if (req.Method != "GET" && req.Method != "POST") || len(req.URL.Path) > len(path) {
return false
}
if req.Method == "GET" && user != nil {
http.Redirect(w, req, "/", 302)
return true
}
ul := viewmodel.UserLogin{}
if req.Method == "POST" {
req.ParseForm()
wa := WikiAthenticator{}
newUser, err := wa.Login(req.Form.Get("username"), req.Form.Get("password"))
if err == nil {
sess := auth.OpenSession(newUser)
http.SetCookie(w, &http.Cookie{Name: auth.SessionCookieName, Value: sess.ID, Expires: sess.Time.Add(auth.SessionMaxTime), Path: "/", HttpOnly: true})
http.Redirect(w, req, "/", 302)
return true
}
ul.UserName = req.Form.Get("username")
ul.Error = err.Error()
}
ul.Setup(user)
view.Render(w, "login", 200, ul)
return true
}
func userLogout(path string, w http.ResponseWriter, req *http.Request, user *auth.User) bool {
//var err error
if user != nil {
auth.CloseSession(user.Session.ID)
}
http.Redirect(w, req, "/", 302)
return true
}
func init() {
UserController.Function("/login", userLogin)
UserController.Function("/logout", userLogout)
}

13
controllers/wikiauth.go

@ -15,14 +15,17 @@ import (
// to log in
type WikiAthenticator struct{}
// ID is for a potential multi-login future
func (wikiAuth *WikiAthenticator) ID() string {
return "wiki"
}
// Name is for a potential multi-login future
func (wikiAuth *WikiAthenticator) Name() string {
return "Wiki"
}
// Find finds a user that has logged in at least once
func (wikiAuth *WikiAthenticator) Find(username string) *auth.User {
db := server.Main.DB
@ -38,7 +41,7 @@ func (wikiAuth *WikiAthenticator) Find(username string) *auth.User {
return nil
}
user := auth.NewUser(wikiAuth, "", "member", nil)
user := auth.NewUser(wikiAuth, "", "member", make(map[string]string, 4))
role := "member"
rows.Scan(&user.ID, &role)
user.Data["role"] = role
@ -63,20 +66,19 @@ func (wikiAuth *WikiAthenticator) Login(username, password string) (*auth.User,
}
// Look up the user
rows, err := db.Query("SELECT id,role FROM `user` WHERE id=?", client.BasicAuthUser)
rows, err := db.Query("SELECT id,role FROM `user` WHERE id=?", username)
if err != nil {
return nil, fmt.Errorf("Login failed %v", err)
return nil, nil
}
// If none was found, just create a new record with the role of member
if !rows.Next() {
_, err = db.Exec("INSERT INTO `user` (id, role) VALUES (?, 'member')", client.BasicAuthUser)
_, err = db.Exec("INSERT INTO `user` (id, role) VALUES (?, 'member')", username)
if err != nil {
return nil, fmt.Errorf("Login failed %v", err)
}
return auth.NewUser(wikiAuth, client.BasicAuthUser, "member", nil), nil
return auth.NewUser(wikiAuth, username, "member", nil), nil
}
// If the user was found, read it in
@ -91,6 +93,7 @@ func (wikiAuth *WikiAthenticator) Login(username, password string) (*auth.User,
return auth.NewUser(wikiAuth, userid, "member", nil), nil
}
// Register just tells the user that they can't.
func (wikiAuth *WikiAthenticator) Register(username, password string, data map[string]string) (*auth.User, error) {
return nil, errors.New("Registration not allowed")
}

BIN
debug

1
main.go

@ -8,6 +8,7 @@ func main() {
router := &server.Main.Router
auth.Register(&controllers.WikiAthenticator{})
router.Mount("/user", &controllers.UserController)
router.Mount("/", &controllers.ListController)
router.Static("/ui/", server.Main.Config.Server.UI)

39
model/category.go

@ -0,0 +1,39 @@
package model
import (
"strings"
)
// PageCategory represents a page category
type PageCategory struct {
Key string
Plural string
Icon string
}
// URLRoot is the "folder" used for searching within the category
func (category *PageCategory) URLRoot() string {
return strings.ToLower(category.Plural)
}
// PageCategories are used by the view model and page to enforce
// a limited selection of categories. I may move it to a configuration
// or the database, but for now I think this list is pretty fixed
var PageCategories = []PageCategory{
{"OoC", "OoC", "ooc"},
{"Info", "Info", "info"},
{"News", "News", "news"},
{"Item", "Items", "item"},
{"Document", "Documents", "document"},
{"Background", "Background", "background"},
{"Story", "Stories", "story"},
}
var pageCategories []string
// init setups pageCategories
func init() {
pageCategories := make([]string, len(PageCategories))
for i, category := range PageCategories {
pageCategories[i] = category.Key
}
}

94
model/header.go

@ -56,9 +56,42 @@ func ListHeaders() ([]Header, error) {
return results, nil
}
// ListHeadersByTag lists all headers that has the tag
func ListHeadersByTag(tag *Tag) ([]Header, error) {
// ListHeadersByCategory grabs all the pages in the given category
func ListHeadersByCategory(category string) ([]Header, error) {
const query = `
SELECT page.id,page.name,author,category,fictional_date,publish_date,edit_date,dated,tag.id,tag.type,tag.name
FROM page
LEFT JOIN page_tag ON (page.id = page_tag.page_id AND page_tag.primary = true)
LEFT JOIN tag ON (tag.id = page_tag.tag_id)
WHERE page.specific=false AND page.published=true AND page.unlisted=false AND page.category = ?;
`
db := server.Main.DB
rows, err := db.Query(query, category)
if err != nil {
return nil, err
}
defer rows.Close()
results := make([]Header, 0, 64)
header := Header{}
for rows.Next() {
err := parseHeader(&header, rows)
if err != nil {
return nil, err
}
results = append(results, header)
}
return results, nil
}
// ListHeadersByTag lists all headers that has the tag. Leave the category empty
// to not filter by it
func ListHeadersByTag(category string, tag *Tag) ([]Header, error) {
const query1 = `
SELECT page.id,page.name,page.author,page.category,page.fictional_date,page.publish_date,page.edit_date,page.dated,tag.id,tag.type,tag.name
FROM page_tag
RIGHT JOIN page ON page.id = page_tag.page_id
@ -66,6 +99,14 @@ func ListHeadersByTag(tag *Tag) ([]Header, error) {
LEFT JOIN (tag AS tag) ON (tag.id = pt2.tag_id)
WHERE page_tag.tag_id=?
`
const query2 = `
SELECT page.id,page.name,page.author,page.category,page.fictional_date,page.publish_date,page.edit_date,page.dated,tag.id,tag.type,tag.name
FROM page_tag
RIGHT JOIN page ON page.id = page_tag.page_id
LEFT JOIN (page_tag AS pt2) ON (page.id = pt2.page_id AND pt2.primary = true)
LEFT JOIN (tag AS tag) ON (tag.id = pt2.tag_id)
WHERE page_tag.tag_id=? AND page.category=?
`
if tag == nil {
return nil, errors.New("no tag")
@ -73,6 +114,11 @@ func ListHeadersByTag(tag *Tag) ([]Header, error) {
db := server.Main.DB
query := query1
if category != "" {
query = query2
}
rows, err := db.Query(query, tag.ID)
if err != nil {
return nil, err
@ -93,6 +139,50 @@ func ListHeadersByTag(tag *Tag) ([]Header, error) {
return results, nil
}
// ListHeadersByTags searches for the first tag, then filters the result based on the
// others. There is room for improvement in this function, but I'll have to judge whether
// there will be a need for that.
func ListHeadersByTags(category string, tags []Tag) ([]Header, error) {
if len(tags) == 0 {
return nil, errors.New("no tags")
}
headers, err := ListHeadersByTag(category, &tags[0])
if err != nil {
return nil, err
}
if len(headers) == 0 {
return headers, nil
}
for _, tag := range tags[1:] {
headers2, err := ListHeadersByTag(category, &tag)
if err != nil {
return nil, err
}
results := make([]Header, 0, len(headers))
for _, header := range headers {
found := false
for _, header2 := range headers2 {
if header.ID == header2.ID {
found = true
break
}
}
if found {
results = append(results, header)
}
}
headers = results
}
return headers, nil
}
func parseHeader(header *Header, rows *sql.Rows) error {
var tagID, tagName, tagType string
var fictionalDate, publishDate, editDate string

16
model/page.go

@ -14,18 +14,6 @@ import (
"github.com/russross/blackfriday"
)
// PageCategories are used by the view model and page to enforce
// a limited selection of categories. I may move it to a configuration
var PageCategories = []string{
"OoC",
"Story",
"Background",
"Document",
"News",
"Item",
"Info",
}
// PageTypes describes how the source is rendered. For now it's only markdown,
// but who knows what the future holds.
var PageTypes = []string{
@ -62,7 +50,7 @@ type Page struct {
// Defaults fills in the default details for a page, suited for populating a form
func (page *Page) Defaults() {
page.Category = PageCategories[0]
page.Category = PageCategories[0].Key
page.Dated = true
page.Published = true
@ -220,7 +208,7 @@ func (page *Page) ParseForm(form url.Values) []error {
errors := make([]error, 0, 4)
page.cachedOutput = ""
err := formparser.Select(form.Get("category"), &page.Category, PageCategories, page.Category != "")
err := formparser.Select(form.Get("category"), &page.Category, pageCategories, page.Category != "")
if err != nil {
errors = append(errors, fmt.Errorf("Category: %s", err))
}

4
model/page_test.go

@ -131,7 +131,7 @@ func TestPage(t *testing.T) {
})
t.Run("ListHeadersByTag", func(t *testing.T) {
headers, err := ListHeadersByTag(testPageTags[1])
headers, err := ListHeadersByTag("", testPageTags[1])
if err != nil {
t.Error(err)
}
@ -160,7 +160,7 @@ func TestPage(t *testing.T) {
}
// Make a fake tag and make sure that doesn't return stuff
headers, err = ListHeadersByTag(&Tag{ID: generate.ID()})
headers, err = ListHeadersByTag("", &Tag{ID: generate.ID()})
if err != nil {
t.Error(err)
}

7
model/tag.go

@ -2,6 +2,7 @@ package model
import (
"errors"
"strings"
"git.aiterp.net/AiteRP/aitestory/server"
"git.aiterp.net/gisle/wrouter/generate"
@ -108,6 +109,12 @@ func (tag *Tag) Validate() error {
return nil
}
// Hook returns the url friendly name, which is pretty much just
// adding underscores to it.
func (tag Tag) Hook() string {
return strings.Replace(tag.Name, " ", "_", -1)
}
// FindTag finds a tag by ID
func FindTag(key string, id string) (*Tag, error) {
db := server.Main.DB

24
model/tag_test.go

@ -96,7 +96,29 @@ func TestTag(t *testing.T) {
t.Run("FindByID", func(t *testing.T) {
tag, err := FindTag("id", id)
if err != nil {
t.Log("Failed to get tags:", err)
t.Log("Failed to find tag:", err)
t.Fail()
}
if tag == nil {
t.Log("No tag found")
t.Fail()
return
}
t.Logf("Tag found: %+v", tag)
if tag.Name != name || tag.ID != id {
t.Error("Incorrect tag")
t.Fail()
}
testTag = tag
})
t.Run("FindByName", func(t *testing.T) {
tag, err := FindTag("name", name)
if err != nil {
t.Log("Failed to find tag:", err)
t.Fail()
}
if tag == nil {

20
view/renderer.go

@ -15,15 +15,22 @@ import (
var wd, _ = os.Getwd()
var rootPath = path.Join(wd, "./view/templates/")
var cache = make(map[string]*template.Template)
var argsCache = make(map[string][]string)
// Register registers a template and compiles it for rendering. This should be done
// in the beginning since an error will terminate the server
func Register(name string, base string) {
tmpl, err := template.New(name).ParseFiles(path.Join(rootPath, name+".tmpl"), path.Join(rootPath, base+".tmpl"))
func Register(name string, base string, fragments ...string) {
for i, fragment := range fragments {
fragments[i] = path.Join(rootPath, fragment+".tmpl")
}
args := append([]string{path.Join(rootPath, name+".tmpl"), path.Join(rootPath, base+".tmpl")}, fragments...)
tmpl, err := template.New(name).ParseFiles(args...)
if err != nil {
log.Fatalf("Failed to register %s: %s", name, err)
}
argsCache[name] = args
cache[name] = tmpl
}
@ -37,8 +44,14 @@ func Render(w http.ResponseWriter, name string, status int, viewModel interface{
return
}
tmpl, err := template.New(name).ParseFiles(argsCache[name]...)
if err != nil {
response.Text(w, 500, "Failed to run template "+name+": "+err.Error())
return
}
w.WriteHeader(status)
err := tmpl.ExecuteTemplate(w, "base", viewModel)
err = tmpl.ExecuteTemplate(w, "base", viewModel)
if err != nil {
log.Println("Template error:", err.Error())
}
@ -56,4 +69,5 @@ func Run(w io.Writer, name string, viewModel interface{}) error {
func init() {
Register("index", "base/default")
Register("login", "base/default")
}

9
view/templates/base/default.tmpl

@ -20,7 +20,7 @@
<script type="text/javascript" src="/ui/js/background.js"></script>
{{ template "head" }}
{{ block "head" . }}{{end}}
</head>
<body>
@ -28,14 +28,11 @@
<div id="content-wrapper">
<main>
{{ template "content" }}
{{ block "content" . }}{{end}}
</main>
<nav class="main-menu">
{{ template "menu" }}
<ul>
<li><a><div class="mg-icon">A</div><div class="mg-label">Login</div></a></li>
</ul>
{{ block "menu" . }}{{end}}
</nav>
</div>
</body>

26
view/templates/index.tmpl

@ -1,11 +1,33 @@
{{ define "content" }}
<article>
<h1>Hello, World</h1>
</article>
{{ end }}
{{ define "menu" }}
<h1>Aite RP</h1>
<a href="/"><h1>Aite RP</h1></a>
<ul>
{{ range .Categories }}
<li class="{{ if eq .Key $.ActiveCategory.Key }}selected{{end}}"><a href="/{{.URLRoot}}/{{$.ActiveTag.Hook}}"><div class="mg-icon">Π</div><div class="mg-label">{{.Plural}}</div></a></li>
{{ end }}
</ul>
{{ if $.User.LoggedIn }}
<ul>
<li><a href="/page/create"><div class="mg-icon">+</div><div class="mg-label">Create</div></a></li>
</ul>
<ul>
<li><a href="/user/logout"><div class="mg-icon">A</div><div class="mg-label">Logout</div></a></li>
</ul>
{{ else }}
<ul>
<li><a href="/user/login"><div class="mg-icon">A</div><div class="mg-label">Login</div></a></li>
</ul>
{{ end }}
{{ end }}
{{ define "head" }}

21
view/templates/login.tmpl

@ -0,0 +1,21 @@
{{ define "content" }}
<article>
<h1>Login</h1>
<form action="/user/login", method="POST">
<p class="red">{{$.Error}}</p>
<input placeholder="Username" name="username" type="text" />
<input placeholder="Password" name="password" type="password" />
<button type="submit">Submit</button>
</form>
</article>
{{ end }}
{{ define "menu" }}
<a href="/"><h1>Aite RP</h1></a>
<li><a href="/"><div class="mg-icon">&lt;</div><div class="mg-label">Back</div></a></li>
{{ end }}
{{ define "head" }}
{{ end }}

22
viewmodel/base.go

@ -2,7 +2,7 @@ package viewmodel
import (
"fmt"
"strings"
"log"
"git.aiterp.net/AiteRP/aitestory/server"
"git.aiterp.net/gisle/wrouter/auth"
@ -10,20 +10,22 @@ import (
// Base is the basic information used to render the page
type Base struct {
UserID string
UserName string
UserRole string
UserLoggedIn bool
ViewTitle string
User struct {
Name string
Role string
LoggedIn bool
}
ViewTitle string
}
// InitBase initializes the base of the viewmodel
func (base *Base) setupBase(user *auth.User, viewTitle string) {
if user != nil {
base.UserID = user.ID
base.UserName = strings.SplitN(user.ID, ":", 2)[1]
base.UserRole = user.Data["role"]
base.UserLoggedIn = true
log.Printf("%+v", user)
base.User.Name = user.ID
base.User.Role = user.Data["role"]
base.User.LoggedIn = true
}
base.ViewTitle = fmt.Sprintf("%s - %s", viewTitle, server.Main.Config.View.Title)

9
viewmodel/pagelist.go

@ -8,10 +8,11 @@ import (
// PageList is a view model for rendering the front page
type PageList struct {
Base
Headers []model.Header
Category string
ActiveTags []model.Tag
FavoriteTags []model.Tag
Headers []model.Header
ActiveCategory model.PageCategory
Categories []model.PageCategory
ActiveTag model.Tag
FavoriteTags []model.Tag
}
// Setup sets up the page model and the base, and should

19
viewmodel/userlogin.go

@ -0,0 +1,19 @@
package viewmodel
import (
"git.aiterp.net/gisle/wrouter/auth"
)
// UserLogin is a view model for rendering the user login form
type UserLogin struct {
Base
Error string
UserName string
Password string
}
// Setup sets up the page model and the base, and should
// be run after the details have been filled in.
func (ul *UserLogin) Setup(user *auth.User) {
ul.setupBase(user, "Login")
}
Loading…
Cancel
Save