Merge pull request 'WIP micropub' (#28) from micropub into main
Reviewed-on: #28
This commit is contained in:
commit
b5dca6fa53
|
@ -8,6 +8,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
@ -249,6 +250,78 @@ func postMediaHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Requ
|
|||
}
|
||||
}
|
||||
|
||||
func userMicropubHandler(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
|
||||
}
|
||||
|
||||
// parse request form
|
||||
err = r.ParseForm()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Bad request"))
|
||||
return
|
||||
}
|
||||
|
||||
// verify access token
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
token = r.Form.Get("access_token")
|
||||
} else {
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
valid, _ := user.ValidateAccessToken(token)
|
||||
if !valid {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
h := r.Form.Get("h")
|
||||
content := r.Form.Get("content")
|
||||
name := r.Form.Get("name")
|
||||
inReplyTo := r.Form.Get("in-reply-to")
|
||||
|
||||
if h != "entry" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Bad request. h must be entry. Got " + h))
|
||||
return
|
||||
}
|
||||
if content == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Bad request. content is required"))
|
||||
return
|
||||
}
|
||||
|
||||
// create post
|
||||
post, err := user.CreateNewPostFull(
|
||||
owl.PostMeta{
|
||||
Title: name,
|
||||
Reply: owl.Reply{
|
||||
Url: inReplyTo,
|
||||
},
|
||||
Date: time.Now(),
|
||||
},
|
||||
content,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Location", post.FullUrl())
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func userMediaHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
filepath := ps.ByName("filepath")
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
package web_test
|
||||
|
||||
import (
|
||||
"h4kor/owl-blogs"
|
||||
main "h4kor/owl-blogs/cmd/owl/web"
|
||||
"h4kor/owl-blogs/test/assertions"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMicropubMinimalArticle(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("h", "entry")
|
||||
form.Add("name", "Test Article")
|
||||
form.Add("content", "Test Content")
|
||||
|
||||
req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusCreated)
|
||||
}
|
||||
|
||||
func TestMicropubWithoutName(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("h", "entry")
|
||||
form.Add("content", "Test Content")
|
||||
|
||||
req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusCreated)
|
||||
loc_header := rr.Header().Get("Location")
|
||||
assertions.Assert(t, loc_header != "", "Location header should be set")
|
||||
}
|
||||
|
||||
func TestMicropubAccessTokenInBody(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("h", "entry")
|
||||
form.Add("content", "Test Content")
|
||||
form.Add("access_token", token)
|
||||
|
||||
req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusCreated)
|
||||
loc_header := rr.Header().Get("Location")
|
||||
assertions.Assert(t, loc_header != "", "Location header should be set")
|
||||
}
|
||||
|
||||
// func TestMicropubAccessTokenInBoth(t *testing.T) {
|
||||
// repo, user := getSingleUserTestRepo()
|
||||
// user.ResetPassword("testpassword")
|
||||
|
||||
// code, _ := user.GenerateAuthCode(
|
||||
// "test", "test", "test", "test", "test",
|
||||
// )
|
||||
// token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
// Code: code,
|
||||
// ClientId: "test",
|
||||
// RedirectUri: "test",
|
||||
// CodeChallenge: "test",
|
||||
// CodeChallengeMethod: "test",
|
||||
// Scope: "test",
|
||||
// })
|
||||
|
||||
// // Create Request and Response
|
||||
// form := url.Values{}
|
||||
// form.Add("h", "entry")
|
||||
// form.Add("content", "Test Content")
|
||||
// form.Add("access_token", token)
|
||||
|
||||
// req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
// req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
// req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
// req.Header.Add("Authorization", "Bearer "+token)
|
||||
// assertions.AssertNoError(t, err, "Error creating request")
|
||||
// rr := httptest.NewRecorder()
|
||||
// router := main.SingleUserRouter(&repo)
|
||||
// router.ServeHTTP(rr, req)
|
||||
|
||||
// assertions.AssertStatus(t, rr, http.StatusBadRequest)
|
||||
// }
|
||||
|
||||
func TestMicropubNoAccessToken(t *testing.T) {
|
||||
repo, user := getSingleUserTestRepo()
|
||||
user.ResetPassword("testpassword")
|
||||
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
|
||||
// Create Request and Response
|
||||
form := url.Values{}
|
||||
form.Add("h", "entry")
|
||||
form.Add("content", "Test Content")
|
||||
|
||||
req, err := http.NewRequest("POST", user.MicropubUrl(), strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode())))
|
||||
assertions.AssertNoError(t, err, "Error creating request")
|
||||
rr := httptest.NewRecorder()
|
||||
router := main.SingleUserRouter(&repo)
|
||||
router.ServeHTTP(rr, req)
|
||||
|
||||
assertions.AssertStatus(t, rr, http.StatusUnauthorized)
|
||||
}
|
|
@ -23,6 +23,7 @@ func Router(repo *owl.Repository) http.Handler {
|
|||
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.NotFound = http.HandlerFunc(notFoundHandler(repo))
|
||||
return router
|
||||
|
@ -41,6 +42,7 @@ func SingleUserRouter(repo *owl.Repository) http.Handler {
|
|||
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.NotFound = http.HandlerFunc(notFoundHandler(repo))
|
||||
return router
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
<link rel="indieauth-metadata" href="{{ .User.IndieauthMetadataUrl }}">
|
||||
<link rel="authorization_endpoint" href="{{ .User.AuthUrl}}">
|
||||
<link rel="token_endpoint" href="{{ .User.TokenUrl}}">
|
||||
<link rel="micropub" href="{{ .User.MicropubUrl}}">
|
||||
{{ end }}
|
||||
<style>
|
||||
header {
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
{{range .}}
|
||||
<div class="h-entry">
|
||||
<hgroup>
|
||||
<h3><a class="u-url" href="{{.UrlPath}}">{{.Title}}</a></h3>
|
||||
<h3><a class="u-url" href="{{.UrlPath}}">
|
||||
{{ if .Title }}{{.Title}}{{ else }}Note: {{.Id}}{{ end }}
|
||||
</a></h3>
|
||||
<small>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Meta.Date}}">
|
||||
|
|
|
@ -86,6 +86,14 @@ func TestCanRenderIndexPage(t *testing.T) {
|
|||
assertions.AssertContains(t, result, "testpost2")
|
||||
}
|
||||
|
||||
func TestCanRenderIndexPageNoTitle(t *testing.T) {
|
||||
user := getTestUser()
|
||||
post, _ := user.CreateNewPostFull(owl.PostMeta{}, "hi")
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
assertions.AssertContains(t, result, post.Id())
|
||||
assertions.AssertContains(t, result, "Note: ")
|
||||
}
|
||||
|
||||
func TestIndexPageContainsHFeedContainer(t *testing.T) {
|
||||
user := getTestUser()
|
||||
user.CreateNewPost("testpost1", false)
|
||||
|
|
45
user.go
45
user.go
|
@ -88,6 +88,11 @@ func (user User) WebmentionUrl() string {
|
|||
return url
|
||||
}
|
||||
|
||||
func (user User) MicropubUrl() string {
|
||||
url, _ := url.JoinPath(user.FullUrl(), "micropub/")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) MediaUrl() string {
|
||||
url, _ := url.JoinPath(user.UrlPath(), "media")
|
||||
return url
|
||||
|
@ -203,8 +208,12 @@ func (user User) GetPost(id string) (*Post, error) {
|
|||
return &post, nil
|
||||
}
|
||||
|
||||
func (user User) CreateNewPost(title string, draft bool) (*Post, error) {
|
||||
folder_name := toDirectoryName(title)
|
||||
func (user User) CreateNewPostFull(meta PostMeta, content string) (*Post, error) {
|
||||
slugHint := meta.Title
|
||||
if slugHint == "" {
|
||||
slugHint = "note"
|
||||
}
|
||||
folder_name := toDirectoryName(slugHint)
|
||||
post_dir := path.Join(user.Dir(), "public", folder_name)
|
||||
|
||||
// if post already exists, add -n to the end of the name
|
||||
|
@ -212,19 +221,13 @@ func (user User) CreateNewPost(title string, draft bool) (*Post, error) {
|
|||
for {
|
||||
if dirExists(post_dir) {
|
||||
i++
|
||||
folder_name = toDirectoryName(fmt.Sprintf("%s-%d", title, i))
|
||||
folder_name = toDirectoryName(fmt.Sprintf("%s-%d", slugHint, i))
|
||||
post_dir = path.Join(user.Dir(), "public", folder_name)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
post := Post{user: &user, id: folder_name, title: title}
|
||||
meta := PostMeta{
|
||||
Title: title,
|
||||
Date: time.Now(),
|
||||
Aliases: []string{},
|
||||
Draft: draft,
|
||||
}
|
||||
post := Post{user: &user, id: folder_name, title: slugHint}
|
||||
|
||||
initial_content := ""
|
||||
initial_content += "---\n"
|
||||
|
@ -236,7 +239,7 @@ func (user User) CreateNewPost(title string, draft bool) (*Post, error) {
|
|||
initial_content += string(meta_bytes)
|
||||
initial_content += "---\n"
|
||||
initial_content += "\n"
|
||||
initial_content += "Write your post here.\n"
|
||||
initial_content += content
|
||||
|
||||
// create post file
|
||||
os.Mkdir(post_dir, 0755)
|
||||
|
@ -246,6 +249,16 @@ func (user User) CreateNewPost(title string, draft bool) (*Post, error) {
|
|||
return &post, nil
|
||||
}
|
||||
|
||||
func (user User) CreateNewPost(title string, draft bool) (*Post, error) {
|
||||
meta := PostMeta{
|
||||
Title: title,
|
||||
Date: time.Now(),
|
||||
Aliases: []string{},
|
||||
Draft: draft,
|
||||
}
|
||||
return user.CreateNewPostFull(meta, title)
|
||||
}
|
||||
|
||||
func (user User) Template() (string, error) {
|
||||
// load base.html
|
||||
path := path.Join(user.Dir(), "meta", "base.html")
|
||||
|
@ -379,3 +392,13 @@ func (user User) GenerateAccessToken(authCode AuthCode) (string, int, error) {
|
|||
Created: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (user User) ValidateAccessToken(token string) (bool, AccessToken) {
|
||||
tokens := user.getAccessTokens()
|
||||
for _, t := range tokens {
|
||||
if t.Token == token {
|
||||
return true, t
|
||||
}
|
||||
}
|
||||
return false, AccessToken{}
|
||||
}
|
||||
|
|
36
user_test.go
36
user_test.go
|
@ -314,3 +314,39 @@ func TestVerifyPassword(t *testing.T) {
|
|||
assertions.Assert(t, !user.VerifyPassword("0000000"), "Password should be incorrect")
|
||||
|
||||
}
|
||||
|
||||
func TestValidateAccessTokenWrongToken(t *testing.T) {
|
||||
user := getTestUser()
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
valid, _ := user.ValidateAccessToken("test")
|
||||
assertions.Assert(t, !valid, "Token should be invalid")
|
||||
}
|
||||
|
||||
func TestValidateAccessTokenCorrectToken(t *testing.T) {
|
||||
user := getTestUser()
|
||||
code, _ := user.GenerateAuthCode(
|
||||
"test", "test", "test", "test", "test",
|
||||
)
|
||||
token, _, _ := user.GenerateAccessToken(owl.AuthCode{
|
||||
Code: code,
|
||||
ClientId: "test",
|
||||
RedirectUri: "test",
|
||||
CodeChallenge: "test",
|
||||
CodeChallengeMethod: "test",
|
||||
Scope: "test",
|
||||
})
|
||||
valid, aToken := user.ValidateAccessToken(token)
|
||||
assertions.Assert(t, valid, "Token should be valid")
|
||||
assertions.Assert(t, aToken.ClientId == "test", "Token should be valid")
|
||||
assertions.Assert(t, aToken.Token == token, "Token should be valid")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue