Merge pull request 'Add CSRF protection to admin panel' (#38) from fix/admin-csrf into main
Reviewed-on: https://git.ts.mattnite.net/mattnite/forgejo-tickets/pulls/38
This commit is contained in:
commit
fc2a3880c4
|
|
@ -77,6 +77,7 @@ func main() {
|
||||||
DB: db,
|
DB: db,
|
||||||
Renderer: renderer,
|
Renderer: renderer,
|
||||||
Auth: authService,
|
Auth: authService,
|
||||||
|
SessionStore: sessionStore,
|
||||||
EmailClient: emailClient,
|
EmailClient: emailClient,
|
||||||
ForgejoClient: forgejoClient,
|
ForgejoClient: forgejoClient,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/mattnite/forgejo-tickets/internal/auth"
|
"github.com/mattnite/forgejo-tickets/internal/auth"
|
||||||
"github.com/mattnite/forgejo-tickets/internal/config"
|
"github.com/mattnite/forgejo-tickets/internal/config"
|
||||||
|
|
@ -15,6 +17,7 @@ type Dependencies struct {
|
||||||
DB *gorm.DB
|
DB *gorm.DB
|
||||||
Renderer *templates.Renderer
|
Renderer *templates.Renderer
|
||||||
Auth *auth.Service
|
Auth *auth.Service
|
||||||
|
SessionStore *auth.PGStore
|
||||||
EmailClient *email.Client
|
EmailClient *email.Client
|
||||||
ForgejoClient *forgejo.Client
|
ForgejoClient *forgejo.Client
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
|
@ -30,30 +33,38 @@ func NewRouter(deps Dependencies) *gin.Engine {
|
||||||
tsAuth := &TailscaleAuth{allowedUsers: deps.Config.TailscaleAllowedUsers}
|
tsAuth := &TailscaleAuth{allowedUsers: deps.Config.TailscaleAllowedUsers}
|
||||||
r.Use(tsAuth.Middleware)
|
r.Use(tsAuth.Middleware)
|
||||||
|
|
||||||
|
csrfSecret := []byte(deps.Config.SessionSecret)
|
||||||
|
isSecure := strings.HasPrefix(deps.Config.BaseURL, "https")
|
||||||
|
csrfMiddleware := middleware.CSRF(csrfSecret, isSecure)
|
||||||
|
|
||||||
|
csrf := r.Group("/")
|
||||||
|
csrf.Use(csrfMiddleware)
|
||||||
|
{
|
||||||
dashboardHandler := &DashboardHandler{deps: deps}
|
dashboardHandler := &DashboardHandler{deps: deps}
|
||||||
r.GET("/", dashboardHandler.Index)
|
csrf.GET("/", dashboardHandler.Index)
|
||||||
|
|
||||||
userHandler := &UserHandler{deps: deps}
|
userHandler := &UserHandler{deps: deps}
|
||||||
r.GET("/users", userHandler.List)
|
csrf.GET("/users", userHandler.List)
|
||||||
r.GET("/users/pending", userHandler.PendingList)
|
csrf.GET("/users/pending", userHandler.PendingList)
|
||||||
r.GET("/users/new", userHandler.NewForm)
|
csrf.GET("/users/new", userHandler.NewForm)
|
||||||
r.GET("/users/:id", userHandler.Detail)
|
csrf.GET("/users/:id", userHandler.Detail)
|
||||||
r.POST("/users", userHandler.Create)
|
csrf.POST("/users", userHandler.Create)
|
||||||
r.POST("/users/:id/approve", userHandler.Approve)
|
csrf.POST("/users/:id/approve", userHandler.Approve)
|
||||||
r.POST("/users/:id/reject", userHandler.Reject)
|
csrf.POST("/users/:id/reject", userHandler.Reject)
|
||||||
r.POST("/users/:id/repos", userHandler.UpdateRepos)
|
csrf.POST("/users/:id/repos", userHandler.UpdateRepos)
|
||||||
|
|
||||||
ticketHandler := &TicketHandler{deps: deps}
|
ticketHandler := &TicketHandler{deps: deps}
|
||||||
r.GET("/tickets", ticketHandler.List)
|
csrf.GET("/tickets", ticketHandler.List)
|
||||||
r.GET("/tickets/:id", ticketHandler.Detail)
|
csrf.GET("/tickets/:id", ticketHandler.Detail)
|
||||||
r.POST("/tickets/:id/status", ticketHandler.UpdateStatus)
|
csrf.POST("/tickets/:id/status", ticketHandler.UpdateStatus)
|
||||||
|
|
||||||
repoHandler := &RepoHandler{deps: deps}
|
repoHandler := &RepoHandler{deps: deps}
|
||||||
r.GET("/repos", repoHandler.List)
|
csrf.GET("/repos", repoHandler.List)
|
||||||
r.GET("/repos/new", repoHandler.NewForm)
|
csrf.GET("/repos/new", repoHandler.NewForm)
|
||||||
r.POST("/repos", repoHandler.Create)
|
csrf.POST("/repos", repoHandler.Create)
|
||||||
r.GET("/repos/:id/edit", repoHandler.EditForm)
|
csrf.GET("/repos/:id/edit", repoHandler.EditForm)
|
||||||
r.POST("/repos/:id", repoHandler.Update)
|
csrf.POST("/repos/:id", repoHandler.Update)
|
||||||
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<form method="POST" action="/repos/{{.Repo.ID}}" class="space-y-6 bg-white p-6 rounded-lg shadow ring-1 ring-gray-200">
|
<form method="POST" action="/repos/{{.Repo.ID}}" class="space-y-6 bg-white p-6 rounded-lg shadow ring-1 ring-gray-200">
|
||||||
|
<input type="hidden" name="gorilla.csrf.Token" value="{{$.CSRFToken}}">
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700">Display Name</label>
|
<label for="name" class="block text-sm font-medium text-gray-700">Display Name</label>
|
||||||
<input type="text" name="name" id="name" required value="{{.Repo.Name}}"
|
<input type="text" name="name" id="name" required value="{{.Repo.Name}}"
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<form method="POST" action="/repos" class="space-y-6 bg-white p-6 rounded-lg shadow ring-1 ring-gray-200">
|
<form method="POST" action="/repos" class="space-y-6 bg-white p-6 rounded-lg shadow ring-1 ring-gray-200">
|
||||||
|
<input type="hidden" name="gorilla.csrf.Token" value="{{.CSRFToken}}">
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700">Display Name</label>
|
<label for="name" class="block text-sm font-medium text-gray-700">Display Name</label>
|
||||||
<input type="text" name="name" id="name" required placeholder="Billing App"
|
<input type="text" name="name" id="name" required placeholder="Billing App"
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@
|
||||||
<!-- Status Update -->
|
<!-- Status Update -->
|
||||||
<div class="mt-6 pt-4 border-t border-gray-200">
|
<div class="mt-6 pt-4 border-t border-gray-200">
|
||||||
<form method="POST" action="/tickets/{{.Ticket.ID}}/status" class="flex items-center gap-3">
|
<form method="POST" action="/tickets/{{.Ticket.ID}}/status" class="flex items-center gap-3">
|
||||||
|
<input type="hidden" name="gorilla.csrf.Token" value="{{$.CSRFToken}}">
|
||||||
<label for="status" class="text-sm font-medium text-gray-700">Update Status:</label>
|
<label for="status" class="text-sm font-medium text-gray-700">Update Status:</label>
|
||||||
<select name="status" id="status" class="rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
<select name="status" id="status" class="rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||||
<option value="open" {{if eq (print .Ticket.Status) "open"}}selected{{end}}>Open</option>
|
<option value="open" {{if eq (print .Ticket.Status) "open"}}selected{{end}}>Open</option>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Project Access</h2>
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">Project Access</h2>
|
||||||
{{if .AllRepos}}
|
{{if .AllRepos}}
|
||||||
<form method="POST" action="/users/{{.User.ID}}/repos" class="bg-white p-6 rounded-lg shadow ring-1 ring-gray-200 mb-8">
|
<form method="POST" action="/users/{{.User.ID}}/repos" class="bg-white p-6 rounded-lg shadow ring-1 ring-gray-200 mb-8">
|
||||||
|
<input type="hidden" name="gorilla.csrf.Token" value="{{$.CSRFToken}}">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{{range .AllRepos}}
|
{{range .AllRepos}}
|
||||||
<label class="flex items-center gap-2">
|
<label class="flex items-center gap-2">
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<form method="POST" action="/users" class="space-y-6 bg-white p-6 rounded-lg shadow ring-1 ring-gray-200">
|
<form method="POST" action="/users" class="space-y-6 bg-white p-6 rounded-lg shadow ring-1 ring-gray-200">
|
||||||
|
<input type="hidden" name="gorilla.csrf.Token" value="{{.CSRFToken}}">
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||||
<input type="text" name="name" id="name" required
|
<input type="text" name="name" id="name" required
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,11 @@
|
||||||
<td class="px-4 py-3 text-right">
|
<td class="px-4 py-3 text-right">
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<form method="POST" action="/users/{{.ID}}/approve">
|
<form method="POST" action="/users/{{.ID}}/approve">
|
||||||
|
<input type="hidden" name="gorilla.csrf.Token" value="{{$.CSRFToken}}">
|
||||||
<button type="submit" class="rounded-md bg-green-600 px-3 py-1.5 text-xs font-semibold text-white shadow hover:bg-green-500">Approve</button>
|
<button type="submit" class="rounded-md bg-green-600 px-3 py-1.5 text-xs font-semibold text-white shadow hover:bg-green-500">Approve</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="POST" action="/users/{{.ID}}/reject" onsubmit="return confirm('Are you sure you want to reject this request? The account will be deleted.')">
|
<form method="POST" action="/users/{{.ID}}/reject" onsubmit="return confirm('Are you sure you want to reject this request? The account will be deleted.')">
|
||||||
|
<input type="hidden" name="gorilla.csrf.Token" value="{{$.CSRFToken}}">
|
||||||
<button type="submit" class="rounded-md bg-red-600 px-3 py-1.5 text-xs font-semibold text-white shadow hover:bg-red-500">Reject</button>
|
<button type="submit" class="rounded-md bg-red-600 px-3 py-1.5 text-xs font-semibold text-white shadow hover:bg-red-500">Reject</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue