commit
f37d02b434
12 changed files with 789 additions and 0 deletions
@ -0,0 +1,152 @@
@@ -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() |
||||
} |
@ -0,0 +1,349 @@
@@ -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)) |
||||
} |
@ -0,0 +1,37 @@
@@ -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 |
||||
} |
@ -0,0 +1,94 @@
@@ -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 |
||||
} |
@ -0,0 +1,19 @@
@@ -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 }} |
@ -0,0 +1,14 @@
@@ -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 }} |
@ -0,0 +1,56 @@
@@ -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> |
@ -0,0 +1,17 @@
@@ -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 }} |
@ -0,0 +1,7 @@
@@ -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 }} |
@ -0,0 +1,25 @@
@@ -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 }} |
@ -0,0 +1,15 @@
@@ -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