Added webhook support and webhook display
This commit is contained in:
parent
89d246f3f6
commit
88d9188055
|
@ -22,29 +22,36 @@ CREATE TABLE IF NOT EXISTS "holiday"
|
||||||
references country(iso_name) on delete cascade on update cascade
|
references country(iso_name) on delete cascade on update cascade
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO "country" (id, iso_name, name)
|
CREATE TABLE IF NOT EXISTS "webhook"
|
||||||
VALUES
|
(
|
||||||
('096ca6c4-5c04-47a4-0063-4b4cc6f6b671', 'HR', 'Croatia'),
|
id uuid,
|
||||||
('096ca6c4-5c04-47a4-0063-4b4cc6f6b672', 'US', 'USA'),
|
created timestamp NOT NULL,
|
||||||
('096ca6c4-5c04-47a4-0063-4b4cc6f6b673', 'FR', 'France'),
|
url varchar(256) NOT NULL,
|
||||||
('096ca6c4-5c04-47a4-0063-4b4cc6f6b674', 'GB', 'Great Britain');
|
country char(2) NOT NULL,
|
||||||
|
retry_count int NOT NULL,
|
||||||
|
on_created bool NOT NULL,
|
||||||
|
on_edited bool NOT NULL,
|
||||||
|
on_deleted bool NOT NULL,
|
||||||
|
|
||||||
INSERT INTO "holiday" (id, country, date, name, description, is_state, is_religious)
|
primary key (id),
|
||||||
VALUES
|
constraint fk_country_id foreign key (country)
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b671', 'HR', '2023-01-01', 'Nova godina', '', true, false),
|
references country(iso_name) on delete cascade on update cascade
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b672', 'HR', '2023-01-06', 'Sveta tri kralja', '', true, true),
|
);
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b673', 'HR', '2023-04-09', 'Uskrs', '', true, true),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b674', 'HR', '2023-04-10', 'Uskrsni ponedjeljak', '', true, true),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b675', 'HR', '2023-05-01', 'Praznik rada', '', true, false),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b676', 'HR', '2023-05-30', 'Dan državnosti', '', true, false),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b677', 'HR', '2023-06-08', 'Tijelovo', '', true, true),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b678', 'HR', '2023-06-22', 'Dan antifašističke borbe', '', true, false),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b679', 'HR', '2023-08-05', 'Dan pobjede i domovinske zahvalnost', '', true, false),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b680', 'HR', '2023-08-15', 'Velika gospa', '', true, true),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b681', 'HR', '2023-11-01', 'Dan svih svetih', '', true, true),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b682', 'HR', '2023-11-18', 'Dan sjećanja na žrtve Domovinskog rata', '', true, false),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b683', 'HR', '2023-12-25', 'Božić', '', true, true),
|
|
||||||
('096ca6c4-5c04-47a4-b363-4b4cc6f6b684', 'HR', '2023-12-26', 'Štefanje', '', true, true);
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "job"
|
||||||
|
(
|
||||||
|
id uuid,
|
||||||
|
webhook_id uuid NOT NULL,
|
||||||
|
|
||||||
|
created timestamp NOT NULL,
|
||||||
|
success bool NOT NULL,
|
||||||
|
success_time timestamp,
|
||||||
|
|
||||||
|
retry_count int NOT NULL,
|
||||||
|
|
||||||
|
content varchar(1024) NOT NULL,
|
||||||
|
|
||||||
|
primary key (id),
|
||||||
|
constraint fk_webhook_id foreign key (webhook_id)
|
||||||
|
references webhook(id) on delete cascade on update cascade
|
||||||
|
);
|
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
INSERT INTO "country" (id, iso_name, name)
|
||||||
|
VALUES
|
||||||
|
('096ca6c4-5c04-47a4-0063-4b4cc6f6b671', 'HR', 'Croatia'),
|
||||||
|
('096ca6c4-5c04-47a4-0063-4b4cc6f6b672', 'US', 'USA'),
|
||||||
|
('096ca6c4-5c04-47a4-0063-4b4cc6f6b673', 'FR', 'France'),
|
||||||
|
('096ca6c4-5c04-47a4-0063-4b4cc6f6b674', 'GB', 'Great Britain');
|
||||||
|
|
||||||
|
INSERT INTO "holiday" (id, country, date, name, description, is_state, is_religious)
|
||||||
|
VALUES
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b671', 'HR', '2023-01-01', 'Nova godina', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b672', 'HR', '2023-01-06', 'Sveta tri kralja', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b673', 'HR', '2023-04-09', 'Uskrs', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b674', 'HR', '2023-04-10', 'Uskrsni ponedjeljak', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b675', 'HR', '2023-05-01', 'Praznik rada', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b676', 'HR', '2023-05-30', 'Dan državnosti', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b677', 'HR', '2023-06-08', 'Tijelovo', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b678', 'HR', '2023-06-22', 'Dan antifašističke borbe', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b679', 'HR', '2023-08-05', 'Dan pobjede i domovinske zahvalnost', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b680', 'HR', '2023-08-15', 'Velika gospa', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b681', 'HR', '2023-11-01', 'Dan svih svetih', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b682', 'HR', '2023-11-18', 'Dan sjećanja na žrtve Domovinskog rata', '', true, false),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b683', 'HR', '2023-12-25', 'Božić', '', true, true),
|
||||||
|
('096ca6c4-5c04-47a4-b363-4b4cc6f6b684', 'HR', '2023-12-26', 'Štefanje', '', true, true);
|
||||||
|
|
||||||
|
INSERT INTO "webhook" (id, created, url, country, retry_count, on_created, on_edited, on_deleted)
|
||||||
|
VALUES
|
||||||
|
('b0b0a6c4-5c04-47a4-b363-4b4cc6f6b671', current_timestamp, 'https://hr.bbr-dev.info', 'HR', 10, true, true, true),
|
||||||
|
('b0b0a6c4-5c04-47a4-b363-4b4cc6f6b672', current_timestamp, 'https://hr2.bbr-dev.info', 'HR', 10, true, false, false);
|
||||||
|
|
||||||
|
INSERT INTO "job" (id, webhook_id, created, success, success_time, retry_count, content)
|
||||||
|
VALUES
|
||||||
|
('c0c0a6c4-5c04-47a4-b363-4b4cc6f6b671', 'b0b0a6c4-5c04-47a4-b363-4b4cc6f6b671', current_timestamp, false, null, 8, '{"type": "created", "holiday": {"id": "096ca6c4-5c04-47a4-b363-4b4cc6f6b671", "name": "Nova godina", "date": "2023-01-01"}}');
|
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "webhook"
|
||||||
|
(
|
||||||
|
id uuid,
|
||||||
|
created timestamp NOT NULL,
|
||||||
|
url varchar(256) NOT NULL,
|
||||||
|
country char(2) NOT NULL,
|
||||||
|
retry_count int NOT NULL,
|
||||||
|
on_created bool NOT NULL,
|
||||||
|
on_edited bool NOT NULL,
|
||||||
|
on_deleted bool NOT NULL,
|
||||||
|
|
||||||
|
primary key (id),
|
||||||
|
constraint fk_country_id foreign key (country)
|
||||||
|
references country(iso_name) on delete cascade on update cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "job"
|
||||||
|
(
|
||||||
|
id uuid,
|
||||||
|
webhook_id uuid NOT NULL,
|
||||||
|
|
||||||
|
created timestamp NOT NULL,
|
||||||
|
success bool NOT NULL,
|
||||||
|
success_time timestamp NOT NULL,
|
||||||
|
|
||||||
|
retry_count int NOT NULL,
|
||||||
|
|
||||||
|
content varchar(1024) NOT NULL,
|
||||||
|
|
||||||
|
primary key (id),
|
||||||
|
constraint fk_webhook_id foreign key (webhook_id)
|
||||||
|
references webhook(id) on delete cascade on update cascade
|
||||||
|
);
|
95
main.go
95
main.go
|
@ -8,6 +8,7 @@ import (
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"holiday-api/holiday"
|
"holiday-api/holiday"
|
||||||
"holiday-api/migration"
|
"holiday-api/migration"
|
||||||
|
"holiday-api/webhook"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -56,10 +57,15 @@ func main() {
|
||||||
holidayService := holiday.HolidayService{DB: client}
|
holidayService := holiday.HolidayService{DB: client}
|
||||||
countryService := holiday.CountryService{DB: client}
|
countryService := holiday.CountryService{DB: client}
|
||||||
yearService := holiday.YearService{DB: client}
|
yearService := holiday.YearService{DB: client}
|
||||||
|
webhookService := webhook.WebhookService{
|
||||||
|
DB: client,
|
||||||
|
Events: make(chan webhook.Event, 10),
|
||||||
|
Authorization: "TODO", // TODO add authorization fetching
|
||||||
|
}
|
||||||
|
|
||||||
g.GET("/api/v1/holidays", getHolidays(holidayService))
|
g.GET("/api/v1/holidays", getHolidays(holidayService))
|
||||||
|
|
||||||
setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService)
|
setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService, webhookService)
|
||||||
|
|
||||||
g.GET("/", func(c *gin.Context) {
|
g.GET("/", func(c *gin.Context) {
|
||||||
year := time.Now().Year()
|
year := time.Now().Year()
|
||||||
|
@ -121,18 +127,23 @@ func loadTemplates(g *gin.Engine) {
|
||||||
|
|
||||||
"templates/admin_dashboard.gohtml",
|
"templates/admin_dashboard.gohtml",
|
||||||
"templates/countries.gohtml",
|
"templates/countries.gohtml",
|
||||||
|
"templates/webhooks.gohtml",
|
||||||
|
|
||||||
"templates/dialogs/add-holiday.gohtml",
|
"templates/dialogs/add-holiday.gohtml",
|
||||||
"templates/dialogs/edit-holiday.gohtml",
|
"templates/dialogs/edit-holiday.gohtml",
|
||||||
"templates/dialogs/delete-holiday.gohtml",
|
"templates/dialogs/delete-holiday.gohtml",
|
||||||
"templates/dialogs/check-is-a-holiday.gohtml",
|
"templates/dialogs/check-is-a-holiday.gohtml",
|
||||||
|
|
||||||
|
"templates/dialogs/add-webhook.gohtml",
|
||||||
|
"templates/dialogs/edit-webhook.gohtml",
|
||||||
|
"templates/dialogs/delete-webhook.gohtml",
|
||||||
|
|
||||||
"templates/dialogs/edit-country.gohtml",
|
"templates/dialogs/edit-country.gohtml",
|
||||||
"templates/dialogs/delete-country.gohtml",
|
"templates/dialogs/delete-country.gohtml",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService) {
|
func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.HolidayService, countryService holiday.CountryService, yearService holiday.YearService, webhookService webhook.WebhookService) {
|
||||||
adminDashboard.Use(gin.BasicAuth(loadAuth()))
|
adminDashboard.Use(gin.BasicAuth(loadAuth()))
|
||||||
|
|
||||||
adminDashboard.GET("/", func(c *gin.Context) {
|
adminDashboard.GET("/", func(c *gin.Context) {
|
||||||
|
@ -252,10 +263,71 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida
|
||||||
c.Redirect(http.StatusSeeOther, "/admin/countries")
|
c.Redirect(http.StatusSeeOther, "/admin/countries")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
adminDashboard.GET("/webhooks", func(c *gin.Context) {
|
||||||
|
webhooks, _ := webhookService.Find()
|
||||||
|
c.HTML(http.StatusOK, "webhooks.gohtml", gin.H{"Webhooks": webhooks})
|
||||||
|
})
|
||||||
|
adminDashboard.POST("/webhooks", func(c *gin.Context) {
|
||||||
|
request := struct {
|
||||||
|
Id *string `form:"id"`
|
||||||
|
Url string `form:"url" binding:"required,min=1"`
|
||||||
|
RetryCount int `form:"retry_count" binding:"required,min=1,max=20"`
|
||||||
|
Country string `form:"country" binding:"len=2"`
|
||||||
|
OnCreated bool `form:"on_created"`
|
||||||
|
OnEdited bool `form:"on_edited"`
|
||||||
|
OnDeleted bool `form:"on_deleted"`
|
||||||
|
}{}
|
||||||
|
if err := c.ShouldBind(&request); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hook := webhook.Webhook{
|
||||||
|
Url: request.Url,
|
||||||
|
Country: request.Country,
|
||||||
|
RetryCount: request.RetryCount,
|
||||||
|
OnCreated: request.OnCreated,
|
||||||
|
OnEdited: request.OnEdited,
|
||||||
|
OnDeleted: request.OnDeleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if request.Id != nil {
|
||||||
|
hook.Id = uuid.MustParse(*request.Id)
|
||||||
|
hook, err = webhookService.Update(hook)
|
||||||
|
} else {
|
||||||
|
hook.Id = uuid.Must(uuid.NewRandom())
|
||||||
|
hook.Created = time.Now()
|
||||||
|
hook, err = webhookService.Create(hook)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
} else {
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/webhooks")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
adminDashboard.POST("/webhooks/:id/delete", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Param("id"))
|
||||||
|
_, err := webhookService.FindById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := webhookService.Delete(id); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "/admin/webhooks")
|
||||||
|
})
|
||||||
|
|
||||||
adminDashboard.GET("/dialogs/add-holiday", func(c *gin.Context) {
|
adminDashboard.GET("/dialogs/add-holiday", func(c *gin.Context) {
|
||||||
countries, _ := countryService.Find()
|
countries, _ := countryService.Find()
|
||||||
c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries})
|
c.HTML(http.StatusOK, "add-holiday.gohtml", gin.H{"Countries": countries})
|
||||||
})
|
})
|
||||||
|
adminDashboard.GET("/dialogs/add-webhook", func(c *gin.Context) {
|
||||||
|
countries, _ := countryService.Find()
|
||||||
|
c.HTML(http.StatusOK, "add-webhook.gohtml", gin.H{"Countries": countries})
|
||||||
|
})
|
||||||
adminDashboard.GET("/dialogs/edit-holiday", func(c *gin.Context) {
|
adminDashboard.GET("/dialogs/edit-holiday", func(c *gin.Context) {
|
||||||
id := uuid.MustParse(c.Query("id"))
|
id := uuid.MustParse(c.Query("id"))
|
||||||
hol, err := service.FindById(id)
|
hol, err := service.FindById(id)
|
||||||
|
@ -266,6 +338,16 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida
|
||||||
countries, _ := countryService.Find()
|
countries, _ := countryService.Find()
|
||||||
c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol})
|
c.HTML(http.StatusOK, "edit-holiday.gohtml", gin.H{"Countries": countries, "Holiday": hol})
|
||||||
})
|
})
|
||||||
|
adminDashboard.GET("/dialogs/edit-webhook", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Query("id"))
|
||||||
|
hook, err := webhookService.FindById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
countries, _ := countryService.Find()
|
||||||
|
c.HTML(http.StatusOK, "edit-webhook.gohtml", gin.H{"Countries": countries, "Webhook": hook})
|
||||||
|
})
|
||||||
adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) {
|
adminDashboard.GET("/dialogs/edit-country", func(c *gin.Context) {
|
||||||
id := uuid.MustParse(c.Query("id"))
|
id := uuid.MustParse(c.Query("id"))
|
||||||
country, err := countryService.FindById(id)
|
country, err := countryService.FindById(id)
|
||||||
|
@ -293,6 +375,15 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country})
|
c.HTML(http.StatusOK, "delete-country.gohtml", gin.H{"Country": country})
|
||||||
})
|
})
|
||||||
|
adminDashboard.GET("/dialogs/delete-webhook", func(c *gin.Context) {
|
||||||
|
id := uuid.MustParse(c.Query("id"))
|
||||||
|
hook, err := webhookService.FindById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "delete-webhook.gohtml", gin.H{"Webhook": hook})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAuth() map[string]string {
|
func loadAuth() map[string]string {
|
||||||
|
|
|
@ -97,8 +97,7 @@ func executeMigration(db *sqlx.DB, name string, script string) error {
|
||||||
var err error = nil
|
var err error = nil
|
||||||
if _, e := tx.Exec(script); e != nil {
|
if _, e := tx.Exec(script); e != nil {
|
||||||
err = e
|
err = e
|
||||||
}
|
} else if _, e := tx.Exec(createMigration, name, hash(script)); e != nil {
|
||||||
if _, e := tx.Exec(createMigration, name, hash(script)); e != nil {
|
|
||||||
err = e
|
err = e
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<section class="container">
|
<section class="container">
|
||||||
<a class="selected" href="#">Search</a>
|
<a class="selected" href="#">Search</a>
|
||||||
<a href="/admin/countries">Countries</a>
|
<a href="/admin/countries">Countries</a>
|
||||||
|
<a href="/admin/webhooks">Webhooks</a>
|
||||||
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
|
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
|
||||||
</section>
|
</section>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<section class="container">
|
<section class="container">
|
||||||
<a href="/admin">Search</a>
|
<a href="/admin">Search</a>
|
||||||
<a class="selected" href="#">Countries</a>
|
<a class="selected" href="#">Countries</a>
|
||||||
|
<a href="/admin/webhooks">Webhooks</a>
|
||||||
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
|
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
|
||||||
</section>
|
</section>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<dialog class="card" id="create-card">
|
||||||
|
<h3 class="card-title">Create webhook</h3>
|
||||||
|
<form method="post" action="/admin/webhooks">
|
||||||
|
<section>
|
||||||
|
<label for="url">Url:</label>
|
||||||
|
<input required id="url" name="url" type="url">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label for="country">Country:</label>
|
||||||
|
<select id="country" required name="country">
|
||||||
|
{{range $entry := .Countries}}
|
||||||
|
<option value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label for="retry_count">Retry count:</label>
|
||||||
|
<input required id="retry_count" name="retry_count" type="number" min="0" max="10">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label class="checkbox"><span>On created</span><input id="on_created" value="true" name="on_created" type="checkbox"></label>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label class="checkbox"><span>On edited</span><input id="on_edited" value="true" name="on_edited" type="checkbox"></label>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label class="checkbox"><span>On deleted</span><input id="on_deleted" value="true" name="on_deleted" type="checkbox"></label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="actions">
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
<button type="button" onclick="closeDialog('#create-card')">Cancel</button>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
<dialog class="card" id="delete-card">
|
||||||
|
<h3 class="card-title">Delete webhook</h3>
|
||||||
|
<p>Are you sure you want to delete webhook to <br><b>{{.Webhook.Url}}?</b><br>All jobs for given webhook will be deleted!</p>
|
||||||
|
<form method="post" action="/admin/webhooks/{{.Webhook.Id}}/delete">
|
||||||
|
<section class="actions">
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
<button type="button" onclick="closeDialog('#delete-card')">Cancel</button>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<dialog class="card" id="update-card">
|
||||||
|
<h3 class="card-title">Edit webhook</h3>
|
||||||
|
<form method="post" action="/admin/webhooks">
|
||||||
|
<input type="hidden" id="id" name="id" value="{{.Webhook.Id}}">
|
||||||
|
<section>
|
||||||
|
<label for="url">Url:</label>
|
||||||
|
<input required id="url" name="url" type="url" value="{{.Webhook.Url}}">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label for="country">Country:</label>
|
||||||
|
<select id="country" required name="country">
|
||||||
|
{{range $entry := .Countries}}
|
||||||
|
<option {{if eq $.Webhook.Country $entry.IsoName}}selected{{end}} value="{{$entry.IsoName}}">{{$entry.Name}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label for="retry_count">Retry count:</label>
|
||||||
|
<input required id="retry_count" name="retry_count" type="number" min="0" max="10" value="{{.Webhook.RetryCount}}">
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label class="checkbox"><span>On created</span><input id="on_created" value="true" name="on_created" type="checkbox" {{if .Webhook.OnCreated}}checked{{end}}></label>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label class="checkbox"><span>On edited</span><input id="on_edited" value="true" name="on_edited" type="checkbox" {{if .Webhook.OnEdited}}checked{{end}}></label>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<label class="checkbox"><span>On deleted</span><input id="on_deleted" value="true" name="on_deleted" type="checkbox" {{if .Webhook.OnDeleted}}checked{{end}}></label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="actions">
|
||||||
|
<button type="submit">Update</button>
|
||||||
|
<button type="button" onclick="closeDialog('#update-card')">Cancel</button>
|
||||||
|
</section>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Holiday-api | Admin dashboard</title>
|
||||||
|
<link rel="stylesheet" href="/assets/style.css">
|
||||||
|
<script src="/assets/global.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="dialog-container"></div>
|
||||||
|
<header>
|
||||||
|
<section class="container">
|
||||||
|
<h1><a href="/">Holiday-api | Webhooks</a></h1>
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
<nav>
|
||||||
|
<section class="container">
|
||||||
|
<a href="/admin">Search</a>
|
||||||
|
<a href="/admin/countries">Countries</a>
|
||||||
|
<a class="selected" href="#">Webhooks</a>
|
||||||
|
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-holiday">Add new holiday</button>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
|
<main>
|
||||||
|
<section style="margin: 1em; flex-grow: 1">
|
||||||
|
|
||||||
|
<button data-type="dialog" data-trigger="#create-card" data-url="/admin/dialogs/add-webhook" style="margin-bottom: 1em; float: right">Create webhook</button>
|
||||||
|
|
||||||
|
<table style="width: 100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Country</th>
|
||||||
|
<th>Url</th>
|
||||||
|
<th>Retry count</th>
|
||||||
|
<th>On create</th>
|
||||||
|
<th>On update</th>
|
||||||
|
<th>On delete</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{{range $entry := .Webhooks}}
|
||||||
|
<tr>
|
||||||
|
<td>{{$entry.Country}}</td>
|
||||||
|
<td>{{$entry.Url}}</td>
|
||||||
|
<td>{{$entry.RetryCount}}</td>
|
||||||
|
<td><img class="icon" src="{{if $entry.OnCreated}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
||||||
|
<td><img class="icon" src="{{if $entry.OnEdited}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
||||||
|
<td><img class="icon" src="{{if $entry.OnDeleted}}/assets/images/done-v.svg{{else}}/assets/images/close-x.svg{{end}}"></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" data-type="dialog" data-trigger="#update-card" data-url="/admin/dialogs/edit-webhook?id={{$entry.Id}}" class="clean icon"><img class="icon" src="/assets/images/edit.svg"></button>
|
||||||
|
<button type="button" data-type="dialog" data-trigger="#delete-card" data-url="/admin/dialogs/delete-webhook?id={{$entry.Id}}" class="clean icon"><img class="icon" src="/assets/images/trash-delete.svg"></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,73 @@
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeCreated EventType = "created"
|
||||||
|
TypeEdited EventType = "edited"
|
||||||
|
TypeDeleted EventType = "deleted"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Type EventType `json:"type"`
|
||||||
|
Holiday struct {
|
||||||
|
Id uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
IsStateHoliday bool `json:"is_state_holiday"`
|
||||||
|
IsReligiousHoliday bool `json:"is_religious_holiday"`
|
||||||
|
} `json:"holiday"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Event) Json() string {
|
||||||
|
content, err := json.Marshal(e)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Webhook struct {
|
||||||
|
Id uuid.UUID `db:"id"`
|
||||||
|
Created time.Time `db:"created"`
|
||||||
|
Url string `db:"url"`
|
||||||
|
Country string `db:"country"`
|
||||||
|
RetryCount int `db:"retry_count"`
|
||||||
|
OnCreated bool `db:"on_created"`
|
||||||
|
OnEdited bool `db:"on_edited"`
|
||||||
|
OnDeleted bool `db:"on_deleted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Webhook) ShouldEmit(e Event) bool {
|
||||||
|
if w.Country == e.Holiday.Country {
|
||||||
|
if e.Type == TypeCreated && w.OnCreated {
|
||||||
|
return true
|
||||||
|
} else if e.Type == TypeEdited && w.OnEdited {
|
||||||
|
return true
|
||||||
|
} else if e.Type == TypeDeleted && w.OnDeleted {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type Job struct {
|
||||||
|
Id uuid.UUID `db:"id"`
|
||||||
|
WebhookId uuid.UUID `db:"webhook_id"`
|
||||||
|
|
||||||
|
Created time.Time `db:"created"`
|
||||||
|
Success bool `db:"success"`
|
||||||
|
SuccessTime *time.Time `db:"success_time"`
|
||||||
|
|
||||||
|
RetryCount int `db:"retry_count"`
|
||||||
|
|
||||||
|
Content string `db:"content"`
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookService struct {
|
||||||
|
DB *sqlx.DB
|
||||||
|
Events <-chan Event
|
||||||
|
Authorization string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) Listen() {
|
||||||
|
for event := range w.Events {
|
||||||
|
webhooks, _ := w.Find()
|
||||||
|
var usedWebhooks []Webhook
|
||||||
|
for _, webhook := range webhooks {
|
||||||
|
usedWebhooks = append(usedWebhooks, webhook)
|
||||||
|
}
|
||||||
|
w.CreateJobs(usedWebhooks, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) Run() {
|
||||||
|
for {
|
||||||
|
jobs, err := w.findRunningJobs()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed fetching running jobs: %v", err)
|
||||||
|
}
|
||||||
|
for _, job := range jobs {
|
||||||
|
job := w.ExecuteJob(job)
|
||||||
|
if err := w.UpdateJob(job); err != nil {
|
||||||
|
log.Printf("failed updating job %v: %v", job.Id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) Find() ([]Webhook, error) {
|
||||||
|
var webhooks []Webhook
|
||||||
|
return webhooks, w.DB.Select(&webhooks, `SELECT * FROM "webhook"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) FindAllJobs() ([]Job, error) {
|
||||||
|
var jobs []Job
|
||||||
|
return jobs, w.DB.Select(&jobs, `SELECT * FROM "job"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) FindJobs(webhookId uuid.UUID) ([]Job, error) {
|
||||||
|
var jobs []Job
|
||||||
|
return jobs, w.DB.Select(&jobs, `SELECT * FROM "job" WHERE id = $1`, webhookId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) findRunningJobs() ([]Job, error) {
|
||||||
|
var jobs []Job
|
||||||
|
return jobs, w.DB.Select(&jobs, `SELECT * FROM "job" WHERE retry_count > 0 ORDER BY created asc`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) Create(webhook Webhook) (Webhook, error) {
|
||||||
|
_, err := w.DB.Exec("INSERT INTO webhook (id, created, url, country, retry_count, on_created, on_edited, on_deleted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||||
|
&webhook.Id, &webhook.Created, &webhook.Url, &webhook.Country, &webhook.RetryCount, &webhook.OnCreated, &webhook.OnEdited, &webhook.OnDeleted,
|
||||||
|
)
|
||||||
|
return webhook, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) Update(webhook Webhook) (Webhook, error) {
|
||||||
|
_, err := w.DB.Exec("UPDATE webhook SET url = $2, country = $3, retry_count = $4, on_created = $5, on_edited = $6, on_deleted = $7 WHERE id = $1",
|
||||||
|
&webhook.Id, &webhook.Url, &webhook.Country, &webhook.RetryCount, &webhook.OnCreated, &webhook.OnEdited, &webhook.OnDeleted,
|
||||||
|
)
|
||||||
|
return webhook, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) CreateJobs(webhooks []Webhook, event Event) {
|
||||||
|
json := event.Json()
|
||||||
|
|
||||||
|
tx, _ := w.DB.Begin()
|
||||||
|
for _, webhook := range webhooks {
|
||||||
|
job := Job{
|
||||||
|
Id: uuid.Must(uuid.NewRandom()),
|
||||||
|
WebhookId: webhook.Id,
|
||||||
|
Created: time.Now(),
|
||||||
|
Success: false,
|
||||||
|
SuccessTime: nil,
|
||||||
|
RetryCount: webhook.RetryCount,
|
||||||
|
Content: json,
|
||||||
|
}
|
||||||
|
_, err := tx.Exec("INSERT INTO job (id, webhook_id, created, success, success_time, retry_count, content) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||||
|
&job.Id, &job.WebhookId, &job.Created, &job.Success, &job.SuccessTime, &job.RetryCount, &job.Content,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) ExecuteJob(job Job) Job {
|
||||||
|
webhook, err := w.FindById(job.WebhookId)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
log.Printf("executing job [%d:%d] %v: POST %s", webhook.RetryCount-job.RetryCount, webhook.RetryCount, job.Id, webhook.Url)
|
||||||
|
request, err := http.NewRequest("POST", webhook.Url, strings.NewReader(job.Content))
|
||||||
|
request.Header.Add("authorization", "Api "+w.Authorization)
|
||||||
|
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
|
if err != nil || response.StatusCode != http.StatusOK {
|
||||||
|
job.RetryCount--
|
||||||
|
} else {
|
||||||
|
job.Success = true
|
||||||
|
currentTime := time.Now()
|
||||||
|
job.SuccessTime = ¤tTime
|
||||||
|
}
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) FindById(webhookId uuid.UUID) (Webhook, error) {
|
||||||
|
var webhook Webhook
|
||||||
|
return webhook, w.DB.Get(&webhook, `SELECT * FROM "webhook" WHERE id = $1`, webhookId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) UpdateJob(job Job) error {
|
||||||
|
_, err := w.DB.Exec(`UPDATE job SET success = $2, success_time = $3, retry_count = $4 WHERE id = $1`,
|
||||||
|
&job.Id, &job.Success, &job.SuccessTime, &job.RetryCount,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebhookService) Delete(webhookId uuid.UUID) error {
|
||||||
|
_, err := w.DB.Exec(`DELETE FROM "webhook" WHERE id = $1`,
|
||||||
|
&webhookId,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
Loading…
Reference in New Issue