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