mirror of
https://github.com/Theodor-Springmann-Stiftung/lenz-web.git
synced 2025-10-28 16:55:32 +00:00
GitHub Webhooks integration
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"debug": true,
|
"debug": true,
|
||||||
"watch": 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
|
package controllers
|
||||||
|
|
||||||
import (
|
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/helpers/middleware"
|
||||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/server"
|
"github.com/Theodor-Springmann-Stiftung/lenz-web/server"
|
||||||
"github.com/Theodor-Springmann-Stiftung/lenz-web/views"
|
"github.com/Theodor-Springmann-Stiftung/lenz-web/views"
|
||||||
@@ -9,11 +10,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ASSETS_URL = "/assets"
|
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{
|
server.Server.Use(ASSETS_URL, compress.New(compress.Config{
|
||||||
Level: compress.LevelBestSpeed,
|
Level: compress.LevelBestSpeed,
|
||||||
}))
|
}))
|
||||||
server.Server.Use(ASSETS_URL, middleware.StaticHandler(&views.StaticFS))
|
server.Server.Use(ASSETS_URL, middleware.StaticHandler(&views.StaticFS))
|
||||||
server.Server.Get("/", GetIndex)
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
@@ -16,6 +17,9 @@ var NoURLProvidedError = errors.New("Missing URL.")
|
|||||||
var NoPathProvidedError = errors.New("Missing path.")
|
var NoPathProvidedError = errors.New("Missing path.")
|
||||||
var NoBranchProvidedError = errors.New("Missing branch name.")
|
var NoBranchProvidedError = errors.New("Missing branch name.")
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
var repo *git.Repository
|
||||||
|
|
||||||
type Commit struct {
|
type Commit struct {
|
||||||
Path string
|
Path string
|
||||||
URL string
|
URL string
|
||||||
@@ -29,7 +33,11 @@ func IsValidRepository(path, url, branch string) *Commit {
|
|||||||
return commit
|
return commit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WARNING: Only OpenOrClone() and Pull() should be used externally.
|
||||||
func OpenOrClone(path, url, branch string) (*Commit, error) {
|
func OpenOrClone(path, url, branch string) (*Commit, error) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
commit := IsValidRepository(path, url, branch)
|
commit := IsValidRepository(path, url, branch)
|
||||||
if commit == nil {
|
if commit == nil {
|
||||||
if _, err := os.Stat(path); err == 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) {
|
func Pull(path, url, branch string) (*Commit, error) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return nil, NoURLProvidedError
|
return nil, NoURLProvidedError
|
||||||
}
|
}
|
||||||
@@ -63,12 +74,19 @@ func Pull(path, url, branch string) (*Commit, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
br := plumbing.NewBranchReferenceName(branch)
|
br := plumbing.NewBranchReferenceName(branch)
|
||||||
repo, err := git.PlainOpen(path)
|
|
||||||
if err != nil {
|
var r *git.Repository
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -82,6 +100,8 @@ func Pull(path, url, branch string) (*Commit, error) {
|
|||||||
}
|
}
|
||||||
defer wt.Clean(&git.CleanOptions{Dir: true})
|
defer wt.Clean(&git.CleanOptions{Dir: true})
|
||||||
|
|
||||||
|
repo = r
|
||||||
|
|
||||||
return latestCommit(repo, path, branch, url)
|
return latestCommit(repo, path, branch, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,14 +118,20 @@ func Read(path, branch, url string) (*Commit, error) {
|
|||||||
return nil, NoURLProvidedError
|
return nil, NoURLProvidedError
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := git.PlainOpen(path)
|
var r *git.Repository
|
||||||
if err != nil {
|
if repo == nil {
|
||||||
return nil, err
|
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)
|
br := plumbing.NewBranchReferenceName(branch)
|
||||||
wt, err := repo.Worktree()
|
wt, err := r.Worktree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func Clone(path, url, branch string) (*Commit, error) {
|
||||||
@@ -141,7 +168,7 @@ func Clone(path, url, branch string) (*Commit, error) {
|
|||||||
|
|
||||||
br := plumbing.NewBranchReferenceName(branch)
|
br := plumbing.NewBranchReferenceName(branch)
|
||||||
|
|
||||||
repo, err := git.PlainClone(path, false, &git.CloneOptions{
|
r, err := git.PlainClone(path, false, &git.CloneOptions{
|
||||||
URL: url,
|
URL: url,
|
||||||
Progress: os.Stdout,
|
Progress: os.Stdout,
|
||||||
})
|
})
|
||||||
@@ -150,7 +177,7 @@ func Clone(path, url, branch string) (*Commit, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
wt, err := repo.Worktree()
|
wt, err := r.Worktree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -163,7 +190,8 @@ func Clone(path, url, branch string) (*Commit, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return latestCommit(repo, path, branch, url)
|
repo = r
|
||||||
|
return latestCommit(r, path, branch, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g Commit) String() string {
|
func (g Commit) String() string {
|
||||||
|
|||||||
4
lenz.go
4
lenz.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -37,6 +38,7 @@ func main() {
|
|||||||
|
|
||||||
dir := filepath.Join(cfg.BaseDIR, cfg.GITPath)
|
dir := filepath.Join(cfg.BaseDIR, cfg.GITPath)
|
||||||
|
|
||||||
|
fmt.Printf("Starting Lenz with config: %v", cfg)
|
||||||
commit, err := gitprovider.OpenOrClone(dir, cfg.GitURL, cfg.GitBranch)
|
commit, err := gitprovider.OpenOrClone(dir, cfg.GitURL, cfg.GitBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -60,7 +62,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server := server.New(engine, storage, cfg.Debug)
|
server := server.New(engine, storage, cfg.Debug)
|
||||||
controllers.Register(server)
|
controllers.Register(server, cfg)
|
||||||
|
|
||||||
server.Start(cfg.Address + ":" + cfg.Port)
|
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(logger.New())
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Use(recover.New())
|
if !debug {
|
||||||
|
server.Use(recover.New())
|
||||||
|
}
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
server.Use(cache.New(cache.Config{
|
server.Use(cache.New(cache.Config{
|
||||||
|
|||||||
@@ -9,6 +9,37 @@
|
|||||||
{{ range $l := $letters -}}
|
{{ range $l := $letters -}}
|
||||||
<div>{{ $l.Letter }}</div>
|
<div>{{ $l.Letter }}</div>
|
||||||
<div>{{ $l.Earliest.Text -}}</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 -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"iter"
|
"iter"
|
||||||
|
"slices"
|
||||||
|
|
||||||
xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
|
xmlparsing "github.com/Theodor-Springmann-Stiftung/lenz-web/xml"
|
||||||
)
|
)
|
||||||
@@ -15,7 +16,7 @@ type Meta struct {
|
|||||||
IsProofread xmlparsing.OptionalBool `xml:"isProofread"`
|
IsProofread xmlparsing.OptionalBool `xml:"isProofread"`
|
||||||
IsDraft xmlparsing.OptionalBool `xml:"isDraft"`
|
IsDraft xmlparsing.OptionalBool `xml:"isDraft"`
|
||||||
Sent []Action `xml:"sent"`
|
Sent []Action `xml:"sent"`
|
||||||
Recieved []Action `xml:"recieved"`
|
Recieved []Action `xml:"received"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Meta) Earliest() *Date {
|
func (m *Meta) Earliest() *Date {
|
||||||
@@ -56,14 +57,23 @@ func (m Meta) String() string {
|
|||||||
return string(json)
|
return string(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Meta) SendRecieved() iter.Seq2[*Action, *Action] {
|
type SendRecievedPair struct {
|
||||||
return func(yield func(*Action, *Action) bool) {
|
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 {
|
for i, sent := range m.Sent {
|
||||||
var rec *Action
|
var rec *Action
|
||||||
if i < len(m.Recieved) {
|
if i < len(m.Recieved) {
|
||||||
rec = &m.Recieved[i]
|
rec = &m.Recieved[i]
|
||||||
}
|
}
|
||||||
if !yield(&sent, rec) {
|
if !yield(SendRecievedPair{Sent: &sent, Received: rec}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user