diff --git a/aitestory.json b/aitestory.json index 24cb70b..8469a1d 100644 --- a/aitestory.json +++ b/aitestory.json @@ -16,7 +16,7 @@ }, "wiki": { - "url": "https://wiki.aiterp.net", + "url": "https://wiki.aiterp.net/w/api.php", "username": "AiteBot@AiteStory", "password": "" }, diff --git a/controllers/listcontroller.go b/controllers/listcontroller.go index a581b6b..2ef1fe8 100644 --- a/controllers/listcontroller.go +++ b/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)) + } } diff --git a/controllers/usercontroller.go b/controllers/usercontroller.go new file mode 100644 index 0000000..d5cdb4e --- /dev/null +++ b/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) +} diff --git a/controllers/wikiauth.go b/controllers/wikiauth.go index 1fc0341..e1de3b9 100644 --- a/controllers/wikiauth.go +++ b/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") } diff --git a/debug b/debug index 32d5133..f54cab3 100755 Binary files a/debug and b/debug differ diff --git a/main.go b/main.go index 31ae977..f3fe7ed 100644 --- a/main.go +++ b/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) diff --git a/model/category.go b/model/category.go new file mode 100644 index 0000000..17be1d7 --- /dev/null +++ b/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 + } +} diff --git a/model/header.go b/model/header.go index e07b29a..029a0eb 100644 --- a/model/header.go +++ b/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 diff --git a/model/page.go b/model/page.go index 9592fd6..a5c9dd5 100644 --- a/model/page.go +++ b/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)) } diff --git a/model/page_test.go b/model/page_test.go index 8db1590..b7c5fa1 100644 --- a/model/page_test.go +++ b/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) } diff --git a/model/tag.go b/model/tag.go index a5425f4..9c5258b 100644 --- a/model/tag.go +++ b/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 diff --git a/model/tag_test.go b/model/tag_test.go index 5d43597..eb048d2 100644 --- a/model/tag_test.go +++ b/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 { diff --git a/view/renderer.go b/view/renderer.go index 5c284e1..15fc60f 100644 --- a/view/renderer.go +++ b/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") } diff --git a/view/templates/base/default.tmpl b/view/templates/base/default.tmpl index 2ae70c4..c05f04a 100644 --- a/view/templates/base/default.tmpl +++ b/view/templates/base/default.tmpl @@ -20,7 +20,7 @@ - {{ template "head" }} + {{ block "head" . }}{{end}} @@ -28,14 +28,11 @@
- {{ template "content" }} + {{ block "content" . }}{{end}}
diff --git a/view/templates/index.tmpl b/view/templates/index.tmpl index 33d8105..7885fe7 100644 --- a/view/templates/index.tmpl +++ b/view/templates/index.tmpl @@ -1,11 +1,33 @@ {{ define "content" }}
-

Hello, World

+
{{ end }} {{ define "menu" }} -

Aite RP

+

Aite RP

+ + + + {{ if $.User.LoggedIn }} + + + + {{ else }} + + {{ end }} + + {{ end }} {{ define "head" }} diff --git a/view/templates/login.tmpl b/view/templates/login.tmpl new file mode 100644 index 0000000..8162347 --- /dev/null +++ b/view/templates/login.tmpl @@ -0,0 +1,21 @@ +{{ define "content" }} +
+

Login

+
+

{{$.Error}}

+ + + +
+
+{{ end }} + +{{ define "menu" }} +

Aite RP

+ +
  • <
    Back
  • +{{ end }} + +{{ define "head" }} + +{{ end }} \ No newline at end of file diff --git a/viewmodel/base.go b/viewmodel/base.go index ecd3174..d367ee1 100644 --- a/viewmodel/base.go +++ b/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) diff --git a/viewmodel/pagelist.go b/viewmodel/pagelist.go index 1c57351..a622d2b 100644 --- a/viewmodel/pagelist.go +++ b/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 diff --git a/viewmodel/userlogin.go b/viewmodel/userlogin.go new file mode 100644 index 0000000..9701847 --- /dev/null +++ b/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") +}