Create wiki system
This commit is contained in:
commit
f37d02b434
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.idea
|
||||||
|
wiki
|
||||||
|
data/
|
||||||
|
session/
|
152
file.go
Normal file
152
file.go
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
|
"html"
|
||||||
|
"html/template"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilePages struct {
|
||||||
|
dirname string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilePages(dirname string) PagesRepository {
|
||||||
|
fp := &FilePages{dirname}
|
||||||
|
return fp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilePages) Get(p string) Page {
|
||||||
|
f, err := os.Open(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
|
||||||
|
if err != nil {
|
||||||
|
return Page{}
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
body, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return Page{}
|
||||||
|
}
|
||||||
|
return Page{Content: string(body)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilePages) Save(p string, page Page, summary, author string) {
|
||||||
|
f, err := os.Create(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
f.WriteString(strings.Replace(page.Content, "\r\n", "\n", -1))
|
||||||
|
|
||||||
|
saveWithGit(fp, p, summary, author)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveWithGit(fp *FilePages, p string, summary, author string) {
|
||||||
|
cmd := exec.Command("git", "add", ".")
|
||||||
|
cmd.Dir = fp.dirname
|
||||||
|
cmd.Run()
|
||||||
|
|
||||||
|
cmd = exec.Command("git", "commit", "-m", "Changes to "+p+" by "+author+"\n\n"+summary)
|
||||||
|
cmd.Dir = fp.dirname
|
||||||
|
cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilePages) Exist(p string) bool {
|
||||||
|
f, err := os.Open(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
|
||||||
|
if err != nil {
|
||||||
|
return os.IsExist(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiffPrettyHtml(diffs []diffmatchpatch.Diff) string {
|
||||||
|
var buff bytes.Buffer
|
||||||
|
for _, diff := range diffs {
|
||||||
|
// text := strings.Replace(html.EscapeString(diff.Text), "\n", "<span class=\"lighter\">¶</span><br>", -1)
|
||||||
|
text := html.EscapeString(diff.Text)
|
||||||
|
switch diff.Type {
|
||||||
|
case diffmatchpatch.DiffInsert:
|
||||||
|
_, _ = buff.WriteString("<ins style=\"background:#e6ffe6;\">")
|
||||||
|
_, _ = buff.WriteString(text)
|
||||||
|
_, _ = buff.WriteString("</ins>")
|
||||||
|
case diffmatchpatch.DiffDelete:
|
||||||
|
_, _ = buff.WriteString("<del style=\"background:#ffe6e6;\">")
|
||||||
|
_, _ = buff.WriteString(text)
|
||||||
|
_, _ = buff.WriteString("</del>")
|
||||||
|
case diffmatchpatch.DiffEqual:
|
||||||
|
_, _ = buff.WriteString("<span>")
|
||||||
|
_, _ = buff.WriteString(text)
|
||||||
|
_, _ = buff.WriteString("</span>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buff.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilePages) PageHistory(p string) ([]Revision, error) {
|
||||||
|
page := strings.Replace(p, " ", "_", -1)
|
||||||
|
cmd := exec.Command("git", "log", "--pretty=oneline", "--no-decorate", "--color=never", page)
|
||||||
|
cmd.Dir = fp.dirname
|
||||||
|
output, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer output.Close()
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Start")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bufio.NewScanner(output)
|
||||||
|
|
||||||
|
var revisions []Revision
|
||||||
|
|
||||||
|
for buf.Scan() {
|
||||||
|
line := buf.Text()
|
||||||
|
start := strings.Index(line, " ")
|
||||||
|
commitId := line[0:start]
|
||||||
|
rest := line[start+1:]
|
||||||
|
pageText := gitRevision(fp.dirname, page, commitId)
|
||||||
|
revisions = append(revisions, Revision{
|
||||||
|
Version: commitId,
|
||||||
|
Page: DiffPage{Content: pageText},
|
||||||
|
Summary: rest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dmp := diffmatchpatch.New()
|
||||||
|
prevText := ""
|
||||||
|
|
||||||
|
for i := len(revisions) - 1; i >= 0; i-- {
|
||||||
|
diffs := dmp.DiffMain(prevText, revisions[i].Page.Content, false)
|
||||||
|
revisions[i].Page.Diff = template.HTML(DiffPrettyHtml(diffs))
|
||||||
|
prevText = revisions[i].Page.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
log.Println("wait")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return revisions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitRevision(dirname, page, version string) string {
|
||||||
|
cmd := exec.Command("git", "show", version+":"+page)
|
||||||
|
cmd.Dir = dirname
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
cmd.Stdout = &buf
|
||||||
|
cmd.Start()
|
||||||
|
cmd.Wait()
|
||||||
|
return buf.String()
|
||||||
|
}
|
349
main.go
Normal file
349
main.go
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gitlab.com/golang-commonmark/markdown"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"p83.nl/go/ekster/pkg/util"
|
||||||
|
"p83.nl/go/indieauth"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mp PagesRepository
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClientID = "https://wiki.p83.nl/"
|
||||||
|
RedirectURI = "https://wiki.p83.nl/auth/callback"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Page
|
||||||
|
type Page struct {
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiffPage struct {
|
||||||
|
Content string
|
||||||
|
Diff template.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
type Revision struct {
|
||||||
|
Version string
|
||||||
|
Page DiffPage
|
||||||
|
Summary string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PagesRepository interface {
|
||||||
|
Get(p string) Page
|
||||||
|
Save(p string, page Page, summary, author string)
|
||||||
|
Exist(p string) bool
|
||||||
|
PageHistory(p string) ([]Revision, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexPage struct {
|
||||||
|
Session *Session
|
||||||
|
Title string
|
||||||
|
Name string
|
||||||
|
Content template.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
type editPage struct {
|
||||||
|
Session *Session
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type historyPage struct {
|
||||||
|
Session *Session
|
||||||
|
Title string
|
||||||
|
Name string
|
||||||
|
History []Revision
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexHandler struct{}
|
||||||
|
type saveHandler struct{}
|
||||||
|
type editHandler struct{}
|
||||||
|
type historyHandler struct{}
|
||||||
|
type authHandler struct{}
|
||||||
|
|
||||||
|
func (*authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
sess, err := NewSession(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sess.Flush()
|
||||||
|
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
if r.URL.Path == "/auth/login" {
|
||||||
|
t, err := template.ParseFiles("templates/layout.html", "templates/login.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = t.Execute(w, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if r.URL.Path == "/auth/callback" {
|
||||||
|
code := r.FormValue("code")
|
||||||
|
state := r.FormValue("state")
|
||||||
|
|
||||||
|
if state != sess.State {
|
||||||
|
http.Error(w, "mismatched state", 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authURL := sess.AuthorizationEndpoint
|
||||||
|
|
||||||
|
verified, response, err := indieauth.VerifyAuthCode(ClientID, code, RedirectURI, authURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !verified {
|
||||||
|
http.Redirect(w, r, "/auth/login", 302)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.Me = response.Me
|
||||||
|
sess.LoggedIn = true
|
||||||
|
sess.State = ""
|
||||||
|
|
||||||
|
http.Redirect(w, r, sess.NextURI, 302)
|
||||||
|
return
|
||||||
|
} else if r.URL.Path == "/auth/logout" {
|
||||||
|
t, err := template.ParseFiles("templates/layout.html", "templates/logout.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = t.Execute(w, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if r.Method == http.MethodPost {
|
||||||
|
if r.URL.Path == "/auth/login" {
|
||||||
|
r.ParseForm()
|
||||||
|
urlString := r.PostFormValue("url")
|
||||||
|
|
||||||
|
u, err := url.Parse(urlString)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := indieauth.GetEndpoints(u)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authURL, err := url.Parse(endpoints.AuthorizationEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := util.RandStringBytes(32)
|
||||||
|
|
||||||
|
sess.AuthorizationEndpoint = authURL.String()
|
||||||
|
sess.Me = urlString
|
||||||
|
sess.LoggedIn = false
|
||||||
|
sess.RedirectURI = RedirectURI
|
||||||
|
sess.NextURI = "/"
|
||||||
|
sess.State = state
|
||||||
|
|
||||||
|
newURL := indieauth.CreateAuthenticationURL(*authURL, urlString, ClientID, RedirectURI, state)
|
||||||
|
|
||||||
|
http.Redirect(w, r, newURL, 302)
|
||||||
|
return
|
||||||
|
} else if r.URL.Path == "/auth/logout" {
|
||||||
|
sess.LoggedIn = false
|
||||||
|
sess.Me = ""
|
||||||
|
sess.AuthorizationEndpoint = ""
|
||||||
|
http.Redirect(w, r, "/", 302)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *historyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
sess, err := NewSession(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sess.Flush()
|
||||||
|
|
||||||
|
r.ParseForm()
|
||||||
|
page := r.URL.Path[9:]
|
||||||
|
if page == "" {
|
||||||
|
page = "Home"
|
||||||
|
}
|
||||||
|
|
||||||
|
history, err := mp.PageHistory(page)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
funcs := template.FuncMap{
|
||||||
|
"historyIndex": func(x int, history []Revision) int { return len(history) - x; },
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.New("layout.html").Funcs(funcs).ParseFiles("templates/layout.html", "templates/history.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.Execute(w, historyPage{
|
||||||
|
Session: sess,
|
||||||
|
Title: "History of " + strings.Replace(page, "_", " ", -1),
|
||||||
|
Name: page,
|
||||||
|
History: history,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *saveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
sess, err := NewSession(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sess.Flush()
|
||||||
|
|
||||||
|
r.ParseForm()
|
||||||
|
page := r.PostForm.Get("p")
|
||||||
|
summary := r.PostForm.Get("summary")
|
||||||
|
|
||||||
|
mp.Save(page, Page{Content: r.PostForm.Get("content")}, summary, sess.Me)
|
||||||
|
http.Redirect(w, r, "/"+page, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *editHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
sess, err := NewSession(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sess.Flush()
|
||||||
|
|
||||||
|
page := r.URL.Path[6:]
|
||||||
|
if page == "" {
|
||||||
|
page = "Home"
|
||||||
|
}
|
||||||
|
|
||||||
|
pageText := mp.Get(page).Content
|
||||||
|
|
||||||
|
data := editPage{
|
||||||
|
Session: sess,
|
||||||
|
Title: strings.Replace(page, "_", " ", -1),
|
||||||
|
Content: pageText,
|
||||||
|
Name: page,
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.ParseFiles("templates/layout.html", "templates/edit.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = t.Execute(w, data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
sess, err := NewSession(w, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sess.Flush()
|
||||||
|
|
||||||
|
page := r.URL.Path[1:]
|
||||||
|
if page == "" {
|
||||||
|
page = "Home"
|
||||||
|
}
|
||||||
|
|
||||||
|
pageText := mp.Get(page).Content
|
||||||
|
if pageText == "" {
|
||||||
|
http.Redirect(w, r, "/edit/"+page, 302)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hrefRE := regexp.MustCompile(`\[\[\s*([\w- ]+)\s*\]\]`)
|
||||||
|
|
||||||
|
pageText = hrefRE.ReplaceAllStringFunc(pageText, func(s string) string {
|
||||||
|
s = strings.TrimPrefix(s, "[[")
|
||||||
|
s = strings.TrimSuffix(s, "]]")
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if !mp.Exist(s) {
|
||||||
|
// return fmt.Sprintf("<a href=%q class=%q>%s</a>", s, "edit", s)
|
||||||
|
return fmt.Sprintf("%s[?](/%s)", s, strings.Replace(s, " ", "_", -1))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s](/%s)", s, strings.Replace(s, " ", "_", -1))
|
||||||
|
})
|
||||||
|
|
||||||
|
md := markdown.New(markdown.XHTMLOutput(true))
|
||||||
|
pageText = md.RenderToString([]byte(pageText))
|
||||||
|
|
||||||
|
data := indexPage{
|
||||||
|
Session: sess,
|
||||||
|
Title: strings.Replace(page, "_", " ", -1),
|
||||||
|
Content: template.HTML(pageText),
|
||||||
|
Name: page,
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.ParseFiles("templates/layout.html", "templates/view.html")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = t.Execute(w, data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mp = NewFilePages("data")
|
||||||
|
|
||||||
|
http.Handle("/auth/", &authHandler{})
|
||||||
|
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("ui/node_modules/trix/dist/"))))
|
||||||
|
http.Handle("/save/", &saveHandler{})
|
||||||
|
http.Handle("/edit/", &editHandler{})
|
||||||
|
http.Handle("/history/", &historyHandler{})
|
||||||
|
http.Handle("/", &indexHandler{})
|
||||||
|
|
||||||
|
fmt.Printf("Running on port 8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
37
memory.go
Normal file
37
memory.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type MemoryPages struct {
|
||||||
|
m sync.RWMutex
|
||||||
|
pages map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// func NewMemoryPages() PagesRepository {
|
||||||
|
// return &MemoryPages{
|
||||||
|
// pages: make(map[string]string),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (mp *MemoryPages) Get(p string) Page {
|
||||||
|
mp.m.RLock()
|
||||||
|
defer mp.m.RUnlock()
|
||||||
|
if pt, e := mp.pages[p]; e {
|
||||||
|
return Page{Content: pt}
|
||||||
|
}
|
||||||
|
return Page{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp *MemoryPages) Save(p string, page Page, summary, author string) {
|
||||||
|
mp.m.Lock()
|
||||||
|
defer mp.m.Unlock()
|
||||||
|
mp.pages[p] = page.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp *MemoryPages) Exist(p string) bool {
|
||||||
|
mp.m.RLock()
|
||||||
|
defer mp.m.RUnlock()
|
||||||
|
_, e := mp.pages[p]
|
||||||
|
return e
|
||||||
|
}
|
94
session.go
Normal file
94
session.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID string
|
||||||
|
LoggedIn bool
|
||||||
|
Me string
|
||||||
|
AuthorizationEndpoint string
|
||||||
|
RedirectURI string
|
||||||
|
State string
|
||||||
|
NextURI string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(w http.ResponseWriter, r *http.Request) (*Session, error) {
|
||||||
|
sessionID, err := getSessionCookie(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
session := &Session{ID: sessionID}
|
||||||
|
err = loadSession(session)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *Session) Flush() error {
|
||||||
|
return saveSession(sess)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveSession(sess *Session) error {
|
||||||
|
filename := generateFilename(sess.ID)
|
||||||
|
err := os.Mkdir("session", 0755)
|
||||||
|
f, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
err = json.NewEncoder(f).Encode(sess)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSession(sess *Session) error {
|
||||||
|
filename := generateFilename(sess.ID)
|
||||||
|
err := os.Mkdir("session", 0755)
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// add defaults to session?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
err = json.NewDecoder(f).Decode(sess)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFilename(id string) string {
|
||||||
|
return fmt.Sprintf("session/%s.json", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSessionCookie(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||||
|
c, err := r.Cookie("session")
|
||||||
|
var sessionVar string
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == http.ErrNoCookie {
|
||||||
|
sessionVar = RandStringBytes(16)
|
||||||
|
newCookie := &http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: sessionVar,
|
||||||
|
Expires: time.Now().Add(24 * time.Hour),
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
|
||||||
|
http.SetCookie(w, newCookie)
|
||||||
|
return sessionVar, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
sessionVar = c.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionVar, nil
|
||||||
|
}
|
19
templates/edit.html
Normal file
19
templates/edit.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<h1 class="title">{{ .Title }}</h1>
|
||||||
|
<form action="/save/" method="post">
|
||||||
|
<input type="hidden" name="p" value="{{ .Name }}" />
|
||||||
|
<textarea name="content" rows="24" style="width:100%">{{ .Content }}</textarea>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Summary</label>
|
||||||
|
<div class="control">
|
||||||
|
<input type="text" name="summary" class="input" placeholder="Summary"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="button is-primary" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content_head" }}
|
||||||
|
{{ end }}
|
14
templates/history.html
Normal file
14
templates/history.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{{ define "navbar" }}
|
||||||
|
<a href="/edit/{{ .Name }}" class="navbar-item">Edit</a>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
{{ range $index, $revision := .History }}
|
||||||
|
<section class="section">
|
||||||
|
<h2 class="title is-4">Revision {{ historyIndex $index $.History }}</h2>
|
||||||
|
<h3 class="subtitle is-6">{{ $revision.Summary }}</h3>
|
||||||
|
<div class="content monospace">{{ $revision.Page.Diff }}</div>
|
||||||
|
</section>
|
||||||
|
<hr/>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
56
templates/layout.html
Normal file
56
templates/layout.html
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css" />
|
||||||
|
<title>{{ .Title }} - Wiki</title>
|
||||||
|
{{ block "content_head" . }} {{ end }}
|
||||||
|
<style>
|
||||||
|
.monospace {
|
||||||
|
font-family: "Fira Code Retina", monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.lighter {
|
||||||
|
color:#ccc;
|
||||||
|
}
|
||||||
|
del {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
ins {
|
||||||
|
display:block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item" href="/">
|
||||||
|
Wiki
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="navbarBasicExample" class="navbar-menu">
|
||||||
|
<div class="navbar-start">
|
||||||
|
{{ block "navbar" . }}{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
{{ template "content" . }}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
17
templates/login.html
Normal file
17
templates/login.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1 class="title is-1">Login</h1>
|
||||||
|
<form action="/auth/login" method="post">
|
||||||
|
<div class="field">
|
||||||
|
<label for="url" class="label">Web Signin</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="url" id="url" placeholder="url, e.g. http://example.com/"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button" type="submit">Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
7
templates/logout.html
Normal file
7
templates/logout.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1 class="title is-1">Logout</h1>
|
||||||
|
|
||||||
|
<form action="/auth/logout" method="post">
|
||||||
|
<button class="button" type="submit">Logout</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
25
templates/view.html
Normal file
25
templates/view.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1 class="title is-1">{{ .Title }}</h1>
|
||||||
|
<div class="content">
|
||||||
|
{{ .Content }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "navbar" }}
|
||||||
|
{{ if $.Session.LoggedIn }}
|
||||||
|
<a href="/edit/{{ .Name }}" class="navbar-item">Edit</a>
|
||||||
|
<a href="/history/{{ .Name }}" class="navbar-item">History</a>
|
||||||
|
<a href="/auth/logout" class="navbar-item">Logout</a>
|
||||||
|
<span class="navbar-item"><b>{{ $.Session.Me }}</b></span>
|
||||||
|
{{ else }}
|
||||||
|
<a href="/auth/login" class="navbar-item">Login</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content_head" }}
|
||||||
|
<style>
|
||||||
|
.edit {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{ end }}
|
15
util.go
Normal file
15
util.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
func RandStringBytes(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user