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 | ||||
| ); | ||||
| 
 | ||||
| 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'); | ||||
| 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, | ||||
| 
 | ||||
| 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); | ||||
|     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, | ||||
| 
 | ||||
|     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 | ||||
| ); | ||||
							
								
								
									
										33
									
								
								db/dev/v1_1.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								db/dev/v1_1.sql
									
									
									
									
									
										Normal file
									
								
							| @ -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"}}'); | ||||
							
								
								
									
										34
									
								
								db/prod/v1_1.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								db/prod/v1_1.sql
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||
| 	"holiday-api/holiday" | ||||
| 	"holiday-api/migration" | ||||
| 	"holiday-api/webhook" | ||||
| 	"html/template" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| @ -56,10 +57,15 @@ func main() { | ||||
| 	holidayService := holiday.HolidayService{DB: client} | ||||
| 	countryService := holiday.CountryService{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)) | ||||
| 
 | ||||
| 	setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService) | ||||
| 	setupAdminDashboard(g.Group("/admin"), holidayService, countryService, yearService, webhookService) | ||||
| 
 | ||||
| 	g.GET("/", func(c *gin.Context) { | ||||
| 		year := time.Now().Year() | ||||
| @ -121,18 +127,23 @@ func loadTemplates(g *gin.Engine) { | ||||
| 
 | ||||
| 		"templates/admin_dashboard.gohtml", | ||||
| 		"templates/countries.gohtml", | ||||
| 		"templates/webhooks.gohtml", | ||||
| 
 | ||||
| 		"templates/dialogs/add-holiday.gohtml", | ||||
| 		"templates/dialogs/edit-holiday.gohtml", | ||||
| 		"templates/dialogs/delete-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/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.GET("/", func(c *gin.Context) { | ||||
| @ -252,10 +263,71 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida | ||||
| 		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) { | ||||
| 		countries, _ := countryService.Find() | ||||
| 		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) { | ||||
| 		id := uuid.MustParse(c.Query("id")) | ||||
| 		hol, err := service.FindById(id) | ||||
| @ -266,6 +338,16 @@ func setupAdminDashboard(adminDashboard *gin.RouterGroup, service holiday.Holida | ||||
| 		countries, _ := countryService.Find() | ||||
| 		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) { | ||||
| 		id := uuid.MustParse(c.Query("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}) | ||||
| 	}) | ||||
| 	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 { | ||||
|  | ||||
| @ -97,8 +97,7 @@ func executeMigration(db *sqlx.DB, name string, script string) error { | ||||
| 	var err error = nil | ||||
| 	if _, e := tx.Exec(script); e != nil { | ||||
| 		err = e | ||||
| 	} | ||||
| 	if _, e := tx.Exec(createMigration, name, hash(script)); e != nil { | ||||
| 	} else if _, e := tx.Exec(createMigration, name, hash(script)); e != nil { | ||||
| 		err = e | ||||
| 	} | ||||
| 	if err != nil { | ||||
|  | ||||
| @ -18,6 +18,7 @@ | ||||
|     <section class="container"> | ||||
|         <a class="selected" href="#">Search</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> | ||||
|     </section> | ||||
| </nav> | ||||
|  | ||||
| @ -18,6 +18,7 @@ | ||||
|     <section class="container"> | ||||
|         <a href="/admin">Search</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> | ||||
|     </section> | ||||
| </nav> | ||||
|  | ||||
							
								
								
									
										35
									
								
								templates/dialogs/add-webhook.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								templates/dialogs/add-webhook.gohtml
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
							
								
								
									
										11
									
								
								templates/dialogs/delete-webhook.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								templates/dialogs/delete-webhook.gohtml
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
							
								
								
									
										36
									
								
								templates/dialogs/edit-webhook.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								templates/dialogs/edit-webhook.gohtml
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
							
								
								
									
										63
									
								
								templates/webhooks.gohtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								templates/webhooks.gohtml
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||
							
								
								
									
										73
									
								
								webhook/model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								webhook/model.go
									
									
									
									
									
										Normal file
									
								
							| @ -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"` | ||||
| } | ||||
							
								
								
									
										141
									
								
								webhook/webhook_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								webhook/webhook_service.go
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user