WIP editor

This commit is contained in:
Niko Abeler 2022-11-29 20:36:50 +01:00
parent b5dca6fa53
commit f380f94043
7 changed files with 399 additions and 13 deletions

View File

@ -0,0 +1,237 @@
package web
import (
"h4kor/owl-blogs"
"net/http"
"time"
"github.com/julienschmidt/httprouter"
)
func isUserLoggedIn(user *owl.User, r *http.Request) bool {
sessionCookie, err := r.Cookie("session")
if err != nil {
return false
}
return user.ValidateSession(sessionCookie.Value)
}
func setCSRFCookie(w http.ResponseWriter) string {
csrfToken := owl.GenerateRandomString(32)
cookie := http.Cookie{
Name: "csrf_token",
Value: csrfToken,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, &cookie)
return csrfToken
}
func checkCSRF(r *http.Request) bool {
// CSRF check
formCsrfToken := r.FormValue("csrf_token")
cookieCsrfToken, err := r.Cookie("csrf_token")
if err != nil {
println("Error getting csrf token from cookie: ", err.Error())
return false
}
if formCsrfToken != cookieCsrfToken.Value {
println("Invalid csrf token")
return false
}
return true
}
func userLoginGetHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
user, err := getUserFromRepo(repo, ps)
if err != nil {
println("Error getting user: ", err.Error())
notFoundHandler(repo)(w, r)
return
}
if isUserLoggedIn(&user, r) {
http.Redirect(w, r, user.EditorUrl(), http.StatusFound)
return
}
csrfToken := setCSRFCookie(w)
html, err := owl.RenderLoginPage(user, csrfToken)
if err != nil {
println("Error rendering login page: ", err.Error())
w.WriteHeader(http.StatusInternalServerError)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "Internal server error",
Message: "Internal server error",
})
w.Write([]byte(html))
return
}
w.Write([]byte(html))
}
}
func userLoginPostHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
user, err := getUserFromRepo(repo, ps)
if err != nil {
println("Error getting user: ", err.Error())
notFoundHandler(repo)(w, r)
return
}
err = r.ParseForm()
if err != nil {
println("Error parsing form: ", err.Error())
w.WriteHeader(http.StatusInternalServerError)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "Internal server error",
Message: "Internal server error",
})
w.Write([]byte(html))
return
}
// CSRF check
if !checkCSRF(r) {
w.WriteHeader(http.StatusBadRequest)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "CSRF Error",
Message: "Invalid csrf token",
})
w.Write([]byte(html))
return
}
password := r.Form.Get("password")
if password == "" {
userLoginGetHandler(repo)(w, r, ps)
return
}
if !user.VerifyPassword(password) {
userLoginGetHandler(repo)(w, r, ps)
return
}
// set session cookie
cookie := http.Cookie{
Name: "session",
Value: user.CreateNewSession(),
Path: "/",
Expires: time.Now().Add(30 * 24 * time.Hour),
HttpOnly: true,
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, user.EditorUrl(), http.StatusFound)
}
}
func userEditorGetHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
user, err := getUserFromRepo(repo, ps)
if err != nil {
println("Error getting user: ", err.Error())
notFoundHandler(repo)(w, r)
return
}
if !isUserLoggedIn(&user, r) {
http.Redirect(w, r, user.EditorLoginUrl(), http.StatusFound)
return
}
csrfToken := setCSRFCookie(w)
html, err := owl.RenderEditorPage(user, csrfToken)
if err != nil {
println("Error rendering editor page: ", err.Error())
w.WriteHeader(http.StatusInternalServerError)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "Internal server error",
Message: "Internal server error",
})
w.Write([]byte(html))
return
}
w.Write([]byte(html))
}
}
func userEditorPostHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
user, err := getUserFromRepo(repo, ps)
if err != nil {
println("Error getting user: ", err.Error())
notFoundHandler(repo)(w, r)
return
}
if !isUserLoggedIn(&user, r) {
http.Redirect(w, r, user.EditorLoginUrl(), http.StatusFound)
return
}
err = r.ParseForm()
if err != nil {
println("Error parsing form: ", err.Error())
w.WriteHeader(http.StatusInternalServerError)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "Internal server error",
Message: "Internal server error",
})
w.Write([]byte(html))
return
}
// CSRF check
if !checkCSRF(r) {
w.WriteHeader(http.StatusBadRequest)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "CSRF Error",
Message: "Invalid csrf token",
})
w.Write([]byte(html))
return
}
// get form values
post_type := r.Form.Get("type")
title := r.Form.Get("title")
description := r.Form.Get("description")
content := r.Form.Get("content")
draft := r.Form.Get("draft")
// validate form values
if post_type == "article" && title == "" {
userEditorGetHandler(repo)(w, r, ps)
return
}
// create post
post, err := user.CreateNewPostFull(owl.PostMeta{
Type: post_type,
Title: title,
Description: description,
Draft: draft == "on",
Date: time.Now(),
}, content)
if err != nil {
println("Error creating post: ", err.Error())
w.WriteHeader(http.StatusInternalServerError)
html, _ := owl.RenderUserError(user, owl.ErrorMessage{
Error: "Internal server error",
Message: "Internal server error",
})
w.Write([]byte(html))
return
}
// redirect to post
if !post.Meta().Draft {
http.Redirect(w, r, post.FullUrl(), http.StatusFound)
} else {
http.Redirect(w, r, user.EditorUrl(), http.StatusFound)
}
}
}

View File

@ -14,16 +14,27 @@ func Router(repo *owl.Repository) http.Handler {
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir())) router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
router.GET("/", repoIndexHandler(repo)) router.GET("/", repoIndexHandler(repo))
router.GET("/user/:user/", userIndexHandler(repo)) router.GET("/user/:user/", userIndexHandler(repo))
// Editor
router.GET("/user/:user/editor/auth/", userLoginGetHandler(repo))
router.POST("/user/:user/editor/auth/", userLoginPostHandler(repo))
router.GET("/user/:user/editor/", userEditorGetHandler(repo))
router.POST("/user/:user/editor/", userEditorPostHandler(repo))
// Media
router.GET("/user/:user/media/*filepath", userMediaHandler(repo))
// RSS
router.GET("/user/:user/index.xml", userRSSHandler(repo))
// Posts
router.GET("/user/:user/posts/:post/", postHandler(repo))
router.GET("/user/:user/posts/:post/media/*filepath", postMediaHandler(repo))
// Webmention
router.POST("/user/:user/webmention/", userWebmentionHandler(repo))
// Micropub
router.POST("/user/:user/micropub/", userMicropubHandler(repo))
// IndieAuth
router.GET("/user/:user/auth/", userAuthHandler(repo)) router.GET("/user/:user/auth/", userAuthHandler(repo))
router.POST("/user/:user/auth/", userAuthProfileHandler(repo)) router.POST("/user/:user/auth/", userAuthProfileHandler(repo))
router.POST("/user/:user/auth/verify/", userAuthVerifyHandler(repo)) router.POST("/user/:user/auth/verify/", userAuthVerifyHandler(repo))
router.POST("/user/:user/auth/token/", userAuthTokenHandler(repo)) router.POST("/user/:user/auth/token/", userAuthTokenHandler(repo))
router.GET("/user/:user/media/*filepath", userMediaHandler(repo))
router.GET("/user/:user/index.xml", userRSSHandler(repo))
router.GET("/user/:user/posts/:post/", postHandler(repo))
router.GET("/user/:user/posts/:post/media/*filepath", postMediaHandler(repo))
router.POST("/user/:user/webmention/", userWebmentionHandler(repo))
router.POST("/user/:user/micropub/", userMicropubHandler(repo))
router.GET("/user/:user/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo)) router.GET("/user/:user/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo))
router.NotFound = http.HandlerFunc(notFoundHandler(repo)) router.NotFound = http.HandlerFunc(notFoundHandler(repo))
return router return router
@ -33,16 +44,27 @@ func SingleUserRouter(repo *owl.Repository) http.Handler {
router := httprouter.New() router := httprouter.New()
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir())) router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
router.GET("/", userIndexHandler(repo)) router.GET("/", userIndexHandler(repo))
// Editor
router.GET("/editor/auth/", userLoginGetHandler(repo))
router.POST("/editor/auth/", userLoginPostHandler(repo))
router.GET("/editor/", userEditorGetHandler(repo))
router.POST("/editor/", userEditorPostHandler(repo))
// Media
router.GET("/media/*filepath", userMediaHandler(repo))
// RSS
router.GET("/index.xml", userRSSHandler(repo))
// Posts
router.GET("/posts/:post/", postHandler(repo))
router.GET("/posts/:post/media/*filepath", postMediaHandler(repo))
// Webmention
router.POST("/webmention/", userWebmentionHandler(repo))
// Micropub
router.POST("/micropub/", userMicropubHandler(repo))
// IndieAuth
router.GET("/auth/", userAuthHandler(repo)) router.GET("/auth/", userAuthHandler(repo))
router.POST("/auth/", userAuthProfileHandler(repo)) router.POST("/auth/", userAuthProfileHandler(repo))
router.POST("/auth/verify/", userAuthVerifyHandler(repo)) router.POST("/auth/verify/", userAuthVerifyHandler(repo))
router.POST("/auth/token/", userAuthTokenHandler(repo)) router.POST("/auth/token/", userAuthTokenHandler(repo))
router.GET("/media/*filepath", userMediaHandler(repo))
router.GET("/index.xml", userRSSHandler(repo))
router.GET("/posts/:post/", postHandler(repo))
router.GET("/posts/:post/media/*filepath", postMediaHandler(repo))
router.POST("/webmention/", userWebmentionHandler(repo))
router.POST("/micropub/", userMicropubHandler(repo))
router.GET("/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo)) router.GET("/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo))
router.NotFound = http.HandlerFunc(notFoundHandler(repo)) router.NotFound = http.HandlerFunc(notFoundHandler(repo))
return router return router

23
embed/editor/editor.html Normal file
View File

@ -0,0 +1,23 @@
<details>
<summary>Write Article</summary>
<form action="" method="post">
<h2>Create New Article</h2>
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<input type="hidden" name="type" value="article">
<label for="title">Title</label>
<input type="text" name="title" placeholder="Title" />
<label for="description">Description</label>
<input type="text" name="description" placeholder="Description" />
<label for="content">Content</label>
<textarea name="content" placeholder="Content" rows="24"></textarea>
<input type="checkbox" name="draft" />
<label for="draft">Draft</label>
<br><br>
<input type="submit" value="Create" />
</form>
</details>
<details>
<summary>Write Note</summary>
TODO
</details>

6
embed/editor/login.html Normal file
View File

@ -0,0 +1,6 @@
<form action="" method="post">
<h2>Login to Editor</h2>
<input type="hidden" name="csrf_token" value="{{.CsrfToken}}">
<input type="password" name="password" />
<input type="submit" value="Login" />
</form>

View File

@ -32,6 +32,7 @@ type Reply struct {
} }
type PostMeta struct { type PostMeta struct {
Type string `yaml:"type"`
Title string `yaml:"title"` Title string `yaml:"title"`
Description string `yaml:"description"` Description string `yaml:"description"`
Aliases []string `yaml:"aliases"` Aliases []string `yaml:"aliases"`
@ -46,6 +47,7 @@ func (pm PostMeta) FormattedDate() string {
func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error { func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error {
type T struct { type T struct {
Type string `yaml:"type"`
Title string `yaml:"title"` Title string `yaml:"title"`
Description string `yaml:"description"` Description string `yaml:"description"`
Aliases []string `yaml:"aliases"` Aliases []string `yaml:"aliases"`
@ -65,6 +67,10 @@ func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err return err
} }
pm.Type = t.Type
if pm.Type == "" {
pm.Type = "article"
}
pm.Title = t.Title pm.Title = t.Title
pm.Description = t.Description pm.Description = t.Description
pm.Aliases = t.Aliases pm.Aliases = t.Aliases

View File

@ -35,6 +35,11 @@ type AuthRequestData struct {
CsrfToken string CsrfToken string
} }
type EditorViewData struct {
User User
CsrfToken string
}
type ErrorMessage struct { type ErrorMessage struct {
Error string Error string
Message string Message string
@ -168,3 +173,33 @@ func RenderUserList(repo Repository) (string, error) {
return renderTemplateStr([]byte(baseTemplate), data) return renderTemplateStr([]byte(baseTemplate), data)
} }
func RenderLoginPage(user User, csrfToken string) (string, error) {
loginHtml, err := renderEmbedTemplate("embed/editor/login.html", EditorViewData{
User: user,
CsrfToken: csrfToken,
})
if err != nil {
return "", err
}
return renderIntoBaseTemplate(user, PageContent{
Title: "Login",
Content: template.HTML(loginHtml),
})
}
func RenderEditorPage(user User, csrfToken string) (string, error) {
editorHtml, err := renderEmbedTemplate("embed/editor/editor.html", EditorViewData{
User: user,
CsrfToken: csrfToken,
})
if err != nil {
return "", err
}
return renderIntoBaseTemplate(user, PageContent{
Title: "Editor",
Content: template.HTML(editorHtml),
})
}

57
user.go
View File

@ -52,6 +52,12 @@ type AccessToken struct {
ExpiresIn int `yaml:"expires_in"` ExpiresIn int `yaml:"expires_in"`
} }
type Session struct {
Id string `yaml:"id"`
Created time.Time `yaml:"created"`
ExpiresIn int `yaml:"expires_in"`
}
func (user User) Dir() string { func (user User) Dir() string {
return path.Join(user.repo.UsersDir(), user.name) return path.Join(user.repo.UsersDir(), user.name)
} }
@ -98,6 +104,16 @@ func (user User) MediaUrl() string {
return url return url
} }
func (user User) EditorUrl() string {
url, _ := url.JoinPath(user.UrlPath(), "editor/")
return url
}
func (user User) EditorLoginUrl() string {
url, _ := url.JoinPath(user.UrlPath(), "editor/auth/")
return url
}
func (user User) PostDir() string { func (user User) PostDir() string {
return path.Join(user.Dir(), "public") return path.Join(user.Dir(), "public")
} }
@ -122,6 +138,10 @@ func (user User) AccessTokensFile() string {
return path.Join(user.MetaDir(), "access_tokens.yml") return path.Join(user.MetaDir(), "access_tokens.yml")
} }
func (user User) SessionsFile() string {
return path.Join(user.MetaDir(), "sessions.yml")
}
func (user User) Name() string { func (user User) Name() string {
return user.name return user.name
} }
@ -397,8 +417,45 @@ func (user User) ValidateAccessToken(token string) (bool, AccessToken) {
tokens := user.getAccessTokens() tokens := user.getAccessTokens()
for _, t := range tokens { for _, t := range tokens {
if t.Token == token { if t.Token == token {
if time.Since(t.Created) < time.Duration(t.ExpiresIn)*time.Second {
return true, t return true, t
} }
} }
}
return false, AccessToken{} return false, AccessToken{}
} }
func (user User) getSessions() []Session {
sessions := make([]Session, 0)
loadFromYaml(user.SessionsFile(), &sessions)
return sessions
}
func (user User) addSession(session Session) error {
sessions := user.getSessions()
sessions = append(sessions, session)
return saveToYaml(user.SessionsFile(), sessions)
}
func (user User) CreateNewSession() string {
// generate code
code := GenerateRandomString(32)
user.addSession(Session{
Id: code,
Created: time.Now(),
ExpiresIn: 30 * 24 * 60 * 60,
})
return code
}
func (user User) ValidateSession(session_id string) bool {
sessions := user.getSessions()
for _, session := range sessions {
if session.Id == session_id {
if time.Since(session.Created) < time.Duration(session.ExpiresIn)*time.Second {
return true
}
}
}
return false
}