mirror of
https://github.com/Theodor-Springmann-Stiftung/lenz-web.git
synced 2025-10-28 08:45:32 +00:00
GitHub Webhooks integration
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"debug": true,
|
||||
"watch": true,
|
||||
"git_url": "git@github.com:Theodor-Springmann-Stiftung/lenz-briefe.git"
|
||||
"git_url": "git@github.com:Theodor-Springmann-Stiftung/lenz-briefe.git",
|
||||
"webhook_secret": "test_secret"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/config"
|
||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/helpers/middleware"
|
||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/server"
|
||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/views"
|
||||
@@ -9,11 +10,21 @@ import (
|
||||
)
|
||||
|
||||
const ASSETS_URL = "/assets"
|
||||
const WBHOOK_URL = "/webhook"
|
||||
|
||||
func Register(server server.Server) {
|
||||
func Register(server server.Server, cfg config.Config) {
|
||||
server.Server.Use(ASSETS_URL, compress.New(compress.Config{
|
||||
Level: compress.LevelBestSpeed,
|
||||
}))
|
||||
server.Server.Use(ASSETS_URL, middleware.StaticHandler(&views.StaticFS))
|
||||
server.Server.Get("/", GetIndex)
|
||||
|
||||
if cfg.WebHookSecret != "" {
|
||||
whurl := WBHOOK_URL
|
||||
if cfg.WebHookEndpoint != "" {
|
||||
whurl = cfg.WebHookEndpoint
|
||||
}
|
||||
server.Server.Post(whurl, PostWebhook(cfg))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
68
controllers/webhook.go
Normal file
68
controllers/webhook.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/config"
|
||||
gitprovider "github.com/Theodor-Springmann-Stiftung/lenz-web/git"
|
||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/xmlmodels"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
const SIGNATURE_PREFIX = "sha256="
|
||||
|
||||
func PostWebhook(cfg config.Config) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
body := c.Body()
|
||||
if !verifySignature256([]byte(cfg.WebHookSecret), body, c.Get("X-Hub-Signature-256")) {
|
||||
return c.SendStatus(fiber.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if c.Get("X-GitHub-Event") == "" {
|
||||
return c.SendStatus(fiber.StatusBadRequest)
|
||||
}
|
||||
|
||||
dir := filepath.Join(cfg.BaseDIR, cfg.GITPath)
|
||||
|
||||
commit, err := gitprovider.Pull(dir, cfg.GitURL, cfg.GitBranch)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
|
||||
_, err = xmlmodels.Parse(dir, commit.Hash)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return c.SendStatus(fiber.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func sign256(secret, body []byte) []byte {
|
||||
computed := hmac.New(sha256.New, secret)
|
||||
computed.Write(body)
|
||||
return []byte(computed.Sum(nil))
|
||||
}
|
||||
|
||||
func verifySignature256(secret, payload []byte, header string) bool {
|
||||
if !strings.HasPrefix(header, SIGNATURE_PREFIX) {
|
||||
return false
|
||||
}
|
||||
|
||||
sig, err := hex.DecodeString(header[len(SIGNATURE_PREFIX):])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
mac.Write(payload)
|
||||
expected := mac.Sum(nil)
|
||||
|
||||
return hmac.Equal(expected, sig)
|
||||
}
|
||||
54
git/git.go
54
git/git.go
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
@@ -16,6 +17,9 @@ var NoURLProvidedError = errors.New("Missing URL.")
|
||||
var NoPathProvidedError = errors.New("Missing path.")
|
||||
var NoBranchProvidedError = errors.New("Missing branch name.")
|
||||
|
||||
var mu sync.Mutex
|
||||
var repo *git.Repository
|
||||
|
||||
type Commit struct {
|
||||
Path string
|
||||
URL string
|
||||
@@ -29,7 +33,11 @@ func IsValidRepository(path, url, branch string) *Commit {
|
||||
return commit
|
||||
}
|
||||
|
||||
// WARNING: Only OpenOrClone() and Pull() should be used externally.
|
||||
func OpenOrClone(path, url, branch string) (*Commit, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
commit := IsValidRepository(path, url, branch)
|
||||
if commit == nil {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
@@ -50,6 +58,9 @@ func OpenOrClone(path, url, branch string) (*Commit, error) {
|
||||
}
|
||||
|
||||
func Pull(path, url, branch string) (*Commit, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if url == "" {
|
||||
return nil, NoURLProvidedError
|
||||
}
|
||||
@@ -63,12 +74,19 @@ func Pull(path, url, branch string) (*Commit, error) {
|
||||
}
|
||||
|
||||
br := plumbing.NewBranchReferenceName(branch)
|
||||
repo, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
var r *git.Repository
|
||||
if repo == nil {
|
||||
rep, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r = rep
|
||||
} else {
|
||||
r = repo
|
||||
}
|
||||
|
||||
wt, err := repo.Worktree()
|
||||
wt, err := r.Worktree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,6 +100,8 @@ func Pull(path, url, branch string) (*Commit, error) {
|
||||
}
|
||||
defer wt.Clean(&git.CleanOptions{Dir: true})
|
||||
|
||||
repo = r
|
||||
|
||||
return latestCommit(repo, path, branch, url)
|
||||
}
|
||||
|
||||
@@ -98,14 +118,20 @@ func Read(path, branch, url string) (*Commit, error) {
|
||||
return nil, NoURLProvidedError
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var r *git.Repository
|
||||
if repo == nil {
|
||||
rep, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r = rep
|
||||
} else {
|
||||
r = repo
|
||||
}
|
||||
|
||||
if err := ValidateBranch(repo, branch); err != nil {
|
||||
if err := ValidateBranch(r, branch); err != nil {
|
||||
br := plumbing.NewBranchReferenceName(branch)
|
||||
wt, err := repo.Worktree()
|
||||
wt, err := r.Worktree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -123,7 +149,8 @@ func Read(path, branch, url string) (*Commit, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return latestCommit(repo, path, branch, url)
|
||||
repo = r
|
||||
return latestCommit(r, path, branch, url)
|
||||
}
|
||||
|
||||
func Clone(path, url, branch string) (*Commit, error) {
|
||||
@@ -141,7 +168,7 @@ func Clone(path, url, branch string) (*Commit, error) {
|
||||
|
||||
br := plumbing.NewBranchReferenceName(branch)
|
||||
|
||||
repo, err := git.PlainClone(path, false, &git.CloneOptions{
|
||||
r, err := git.PlainClone(path, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
Progress: os.Stdout,
|
||||
})
|
||||
@@ -150,7 +177,7 @@ func Clone(path, url, branch string) (*Commit, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wt, err := repo.Worktree()
|
||||
wt, err := r.Worktree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -163,7 +190,8 @@ func Clone(path, url, branch string) (*Commit, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return latestCommit(repo, path, branch, url)
|
||||
repo = r
|
||||
return latestCommit(r, path, branch, url)
|
||||
}
|
||||
|
||||
func (g Commit) String() string {
|
||||
|
||||
4
lenz.go
4
lenz.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -37,6 +38,7 @@ func main() {
|
||||
|
||||
dir := filepath.Join(cfg.BaseDIR, cfg.GITPath)
|
||||
|
||||
fmt.Printf("Starting Lenz with config: %v", cfg)
|
||||
commit, err := gitprovider.OpenOrClone(dir, cfg.GitURL, cfg.GitBranch)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -60,7 +62,7 @@ func main() {
|
||||
}
|
||||
|
||||
server := server.New(engine, storage, cfg.Debug)
|
||||
controllers.Register(server)
|
||||
controllers.Register(server, cfg)
|
||||
|
||||
server.Start(cfg.Address + ":" + cfg.Port)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ func New(engine *templating.Engine, storage fiber.Storage, debug bool) Server {
|
||||
server.Use(logger.New())
|
||||
}
|
||||
|
||||
server.Use(recover.New())
|
||||
if !debug {
|
||||
server.Use(recover.New())
|
||||
}
|
||||
|
||||
if debug {
|
||||
server.Use(cache.New(cache.Config{
|
||||
|
||||
@@ -9,6 +9,37 @@
|
||||
{{ range $l := $letters -}}
|
||||
<div>{{ $l.Letter }}</div>
|
||||
<div>{{ $l.Earliest.Text -}}</div>
|
||||
{{- range $sr := $l.SendReceivedPairs -}}
|
||||
<div>
|
||||
<div>
|
||||
{{- range $i, $p := $sr.Sent.Persons -}}
|
||||
<div>
|
||||
{{- if $i -}}
|
||||
,
|
||||
{{ end -}}
|
||||
{{- $person := Person $p.Reference -}}
|
||||
{{- $person.Name -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
<div>an</div>
|
||||
{{- if $sr.Received -}}
|
||||
<div>
|
||||
{{- range $i, $p := $sr.Received.Persons -}}
|
||||
<div>
|
||||
{{- if $i -}}
|
||||
,
|
||||
{{ end -}}
|
||||
{{- $person := Person $p.Reference -}}
|
||||
{{- $person.Name -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- else -}}
|
||||
<div>Unbekannt</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
|
||||
)
|
||||
@@ -15,7 +16,7 @@ type Meta struct {
|
||||
IsProofread xmlparsing.OptionalBool `xml:"isProofread"`
|
||||
IsDraft xmlparsing.OptionalBool `xml:"isDraft"`
|
||||
Sent []Action `xml:"sent"`
|
||||
Recieved []Action `xml:"recieved"`
|
||||
Recieved []Action `xml:"received"`
|
||||
}
|
||||
|
||||
func (m *Meta) Earliest() *Date {
|
||||
@@ -56,14 +57,23 @@ func (m Meta) String() string {
|
||||
return string(json)
|
||||
}
|
||||
|
||||
func (m Meta) SendRecieved() iter.Seq2[*Action, *Action] {
|
||||
return func(yield func(*Action, *Action) bool) {
|
||||
type SendRecievedPair struct {
|
||||
Sent *Action
|
||||
Received *Action
|
||||
}
|
||||
|
||||
func (m Meta) SendReceivedPairs() []SendRecievedPair {
|
||||
return slices.Collect(m.SendRecieved())
|
||||
}
|
||||
|
||||
func (m Meta) SendRecieved() iter.Seq[SendRecievedPair] {
|
||||
return func(yield func(SendRecievedPair) bool) {
|
||||
for i, sent := range m.Sent {
|
||||
var rec *Action
|
||||
if i < len(m.Recieved) {
|
||||
rec = &m.Recieved[i]
|
||||
}
|
||||
if !yield(&sent, rec) {
|
||||
if !yield(SendRecievedPair{Sent: &sent, Received: rec}) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user