Setting up webhooks
This commit is contained in:
parent
50b0b29e10
commit
c14cff4f51
|
|
@ -2,6 +2,8 @@ package forgejo
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -45,6 +47,14 @@ type Comment struct {
|
|||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func GenerateWebhookSecret() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateIssue(owner, repo string, req CreateIssueRequest) (*Issue, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues", c.baseURL, owner, repo)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mattnite/forgejo-tickets/internal/forgejo"
|
||||
"github.com/mattnite/forgejo-tickets/internal/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
|
@ -37,17 +38,28 @@ func (h *RepoHandler) Create(c *gin.Context) {
|
|||
slug := strings.TrimSpace(c.PostForm("slug"))
|
||||
forgejoOwner := strings.TrimSpace(c.PostForm("forgejo_owner"))
|
||||
forgejoRepo := strings.TrimSpace(c.PostForm("forgejo_repo"))
|
||||
webhookSecret := strings.TrimSpace(c.PostForm("webhook_secret"))
|
||||
active := c.PostForm("active") == "on"
|
||||
|
||||
if name == "" || slug == "" || forgejoOwner == "" || forgejoRepo == "" || webhookSecret == "" {
|
||||
if name == "" || slug == "" || forgejoOwner == "" || forgejoRepo == "" {
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "admin/repos/new", map[string]interface{}{
|
||||
"Error": "All fields are required",
|
||||
"Name": name,
|
||||
"Slug": slug,
|
||||
"ForgejoOwner": forgejoOwner,
|
||||
"ForgejoRepo": forgejoRepo,
|
||||
"WebhookSecret": webhookSecret,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
webhookSecret, err := forgejo.GenerateWebhookSecret()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("generate webhook secret error")
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "admin/repos/new", map[string]interface{}{
|
||||
"Error": "Failed to generate webhook secret",
|
||||
"Name": name,
|
||||
"Slug": slug,
|
||||
"ForgejoOwner": forgejoOwner,
|
||||
"ForgejoRepo": forgejoRepo,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -69,7 +81,6 @@ func (h *RepoHandler) Create(c *gin.Context) {
|
|||
"Slug": slug,
|
||||
"ForgejoOwner": forgejoOwner,
|
||||
"ForgejoRepo": forgejoRepo,
|
||||
"WebhookSecret": webhookSecret,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -93,6 +104,7 @@ func (h *RepoHandler) EditForm(c *gin.Context) {
|
|||
h.deps.Renderer.Render(c.Writer, c.Request, "admin/repos/edit", map[string]interface{}{
|
||||
"Repo": repo,
|
||||
"BaseURL": h.deps.Config.BaseURL,
|
||||
"ForgejoURL": h.deps.Config.ForgejoURL,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +119,6 @@ func (h *RepoHandler) Update(c *gin.Context) {
|
|||
slug := strings.TrimSpace(c.PostForm("slug"))
|
||||
forgejoOwner := strings.TrimSpace(c.PostForm("forgejo_owner"))
|
||||
forgejoRepo := strings.TrimSpace(c.PostForm("forgejo_repo"))
|
||||
webhookSecret := strings.TrimSpace(c.PostForm("webhook_secret"))
|
||||
active := c.PostForm("active") == "on"
|
||||
|
||||
if err := h.deps.DB.Model(&models.Repo{}).Where("id = ?", repoID).Updates(map[string]interface{}{
|
||||
|
|
@ -115,7 +126,6 @@ func (h *RepoHandler) Update(c *gin.Context) {
|
|||
"slug": slug,
|
||||
"forgejo_owner": forgejoOwner,
|
||||
"forgejo_repo": forgejoRepo,
|
||||
"webhook_secret": webhookSecret,
|
||||
"active": active,
|
||||
}).Error; err != nil {
|
||||
log.Error().Err(err).Msg("update repo error")
|
||||
|
|
|
|||
|
|
@ -15,12 +15,24 @@ type AuthHandler struct {
|
|||
deps Dependencies
|
||||
}
|
||||
|
||||
func (h *AuthHandler) loginData(extra map[string]interface{}) map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"GoogleEnabled": h.deps.Config.GoogleClientID != "",
|
||||
"MicrosoftEnabled": h.deps.Config.MicrosoftClientID != "",
|
||||
"AppleEnabled": h.deps.Config.AppleClientID != "",
|
||||
}
|
||||
for k, v := range extra {
|
||||
data[k] = v
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (h *AuthHandler) LoginForm(c *gin.Context) {
|
||||
if auth.CurrentUser(c) != nil {
|
||||
c.Redirect(http.StatusSeeOther, "/tickets")
|
||||
return
|
||||
}
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "login", nil)
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "login", h.loginData(nil))
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
|
|
@ -29,19 +41,19 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
|||
|
||||
user, err := h.deps.Auth.Login(c.Request.Context(), email, password)
|
||||
if err != nil {
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "login", map[string]interface{}{
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "login", h.loginData(map[string]interface{}{
|
||||
"Error": err.Error(),
|
||||
"Email": email,
|
||||
})
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.deps.Auth.CreateSession(c.Request, c.Writer, user.ID); err != nil {
|
||||
log.Error().Err(err).Msg("create session error")
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "login", map[string]interface{}{
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "login", h.loginData(map[string]interface{}{
|
||||
"Error": "An unexpected error occurred",
|
||||
"Email": email,
|
||||
})
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package public
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mattnite/forgejo-tickets/internal/forgejo"
|
||||
|
|
@ -30,6 +31,15 @@ func (h *WebhookHandler) HandleForgejoWebhook(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if !repo.WebhookVerified {
|
||||
now := time.Now()
|
||||
h.deps.DB.Model(&repo).Updates(map[string]interface{}{
|
||||
"webhook_verified": true,
|
||||
"webhook_verified_at": now,
|
||||
})
|
||||
log.Info().Msgf("webhook: marked repo %q as verified", repoSlug)
|
||||
}
|
||||
|
||||
payload, err := forgejo.ParseWebhookPayload(body)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("webhook: parse error")
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ type Repo struct {
|
|||
ForgejoOwner string `gorm:"not null" json:"forgejo_owner"`
|
||||
ForgejoRepo string `gorm:"not null" json:"forgejo_repo"`
|
||||
WebhookSecret string `gorm:"not null" json:"webhook_secret"`
|
||||
WebhookVerified bool `gorm:"not null;default:false" json:"webhook_verified"`
|
||||
WebhookVerifiedAt *time.Time `json:"webhook_verified_at"`
|
||||
Active bool `gorm:"not null;default:true" json:"active"`
|
||||
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,38 @@
|
|||
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Edit Repo</h1>
|
||||
|
||||
<div class="mb-6 rounded-md bg-blue-50 p-4">
|
||||
<p class="text-sm text-blue-800">
|
||||
<strong>Webhook URL:</strong>
|
||||
<code class="ml-1 font-mono text-xs">{{.BaseURL}}/webhooks/forgejo/{{.Repo.Slug}}</code>
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-blue-600">Configure this URL in Forgejo's webhook settings for this repo.</p>
|
||||
{{if .Repo.WebhookVerified}}
|
||||
<div class="mb-6 rounded-md bg-green-50 p-4 ring-1 ring-green-200">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="text-sm font-semibold text-green-800">Webhook verified</p>
|
||||
</div>
|
||||
{{if .Repo.WebhookVerifiedAt}}
|
||||
<p class="mt-1 text-xs text-green-600">Verified at {{.Repo.WebhookVerifiedAt.Format "Jan 02, 2006 3:04 PM"}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="mb-6 rounded-md bg-yellow-50 p-4 ring-1 ring-yellow-200">
|
||||
<p class="text-sm font-semibold text-yellow-800 mb-3">Webhook setup required</p>
|
||||
<p class="text-sm text-yellow-700 mb-3">Follow these steps to configure the Forgejo webhook:</p>
|
||||
<ol class="list-decimal list-inside space-y-2 text-sm text-yellow-700">
|
||||
<li>Go to <a href="{{.ForgejoURL}}/{{.Repo.ForgejoOwner}}/{{.Repo.ForgejoRepo}}/settings/hooks" target="_blank" class="text-blue-600 underline hover:text-blue-500">repo webhook settings in Forgejo</a></li>
|
||||
<li>Click <strong>Add Webhook</strong> → <strong>Forgejo</strong></li>
|
||||
<li>Set <strong>Target URL</strong> to:
|
||||
<code class="block mt-1 bg-yellow-100 px-2 py-1 rounded text-xs font-mono select-all">{{.BaseURL}}/webhooks/forgejo/{{.Repo.Slug}}</code>
|
||||
</li>
|
||||
<li>Set <strong>Content Type</strong> to <code class="font-mono text-xs">application/json</code></li>
|
||||
<li>Set <strong>Secret</strong> to:
|
||||
<code class="block mt-1 bg-yellow-100 px-2 py-1 rounded text-xs font-mono select-all">{{.Repo.WebhookSecret}}</code>
|
||||
</li>
|
||||
<li>Under <strong>Trigger On</strong>, select <strong>Custom Events</strong> → check only <strong>Issues</strong></li>
|
||||
<li>Click <strong>Add Webhook</strong>, then click into the webhook and hit <strong>Test Delivery</strong></li>
|
||||
<li>Come back and <strong>reload this page</strong> to confirm verification</li>
|
||||
</ol>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="/repos/{{.Repo.ID}}" class="space-y-6 bg-white p-6 rounded-lg shadow ring-1 ring-gray-200">
|
||||
<div>
|
||||
|
|
@ -42,12 +67,6 @@
|
|||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="webhook_secret" class="block text-sm font-medium text-gray-700">Webhook Secret</label>
|
||||
<input type="text" name="webhook_secret" id="webhook_secret" required value="{{.Repo.WebhookSecret}}"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" name="active" id="active" {{if .Repo.Active}}checked{{end}}
|
||||
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Forgejo</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Webhook URL</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Webhook</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Active</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
|
|
@ -24,7 +24,13 @@
|
|||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{.Name}}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500">{{.ForgejoOwner}}/{{.ForgejoRepo}}</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500 font-mono text-xs">/webhooks/forgejo/{{.Slug}}</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{{if .WebhookVerified}}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Verified</span>
|
||||
{{else}}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Unverified</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
{{if .Active}}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span>
|
||||
|
|
|
|||
|
|
@ -46,12 +46,8 @@
|
|||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="webhook_secret" class="block text-sm font-medium text-gray-700">Webhook Secret</label>
|
||||
<input type="text" name="webhook_secret" id="webhook_secret" required
|
||||
value="{{with .Data}}{{.WebhookSecret}}{{end}}"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-500">Must match the secret configured in Forgejo's webhook settings</p>
|
||||
<div class="rounded-md bg-blue-50 p-4">
|
||||
<p class="text-sm text-blue-800">After adding, you'll be shown instructions for setting up the Forgejo webhook.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -31,17 +31,21 @@
|
|||
<button type="submit" class="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-500">Sign in</button>
|
||||
</form>
|
||||
|
||||
{{with .Data}}
|
||||
{{if or .GoogleEnabled .MicrosoftEnabled .AppleEnabled}}
|
||||
<div class="mt-6">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center"><div class="w-full border-t border-gray-300"></div></div>
|
||||
<div class="relative flex justify-center text-sm"><span class="bg-gray-50 px-2 text-gray-500">Or continue with</span></div>
|
||||
</div>
|
||||
<div class="mt-6 grid grid-cols-3 gap-3">
|
||||
<a href="/auth/google/login" class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50">Google</a>
|
||||
<a href="/auth/microsoft/login" class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50">Microsoft</a>
|
||||
<a href="/auth/apple/login" class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50">Apple</a>
|
||||
<div class="mt-6 flex flex-wrap gap-3 justify-center">
|
||||
{{if .GoogleEnabled}}<a href="/auth/google/login" class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50">Google</a>{{end}}
|
||||
{{if .MicrosoftEnabled}}<a href="/auth/microsoft/login" class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50">Microsoft</a>{{end}}
|
||||
{{if .AppleEnabled}}<a href="/auth/apple/login" class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50">Apple</a>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<p class="mt-6 text-center text-sm text-gray-500">
|
||||
Don't have an account? <a href="/register" class="font-medium text-blue-600 hover:text-blue-500">Register</a>
|
||||
|
|
|
|||
Loading…
Reference in New Issue