diff --git a/config/config.go b/config/config.go index eccf204..785554a 100644 --- a/config/config.go +++ b/config/config.go @@ -3,6 +3,7 @@ package config import ( "fmt" "os" + "strings" "time" "github.com/spf13/viper" @@ -18,6 +19,7 @@ type Config struct { SchedulerJobs SchedulerConfig `mapstructure:"scheduler_jobs" yaml:"scheduler_jobs"` EmailConfig EmailConfig `mapstructure:"email" yaml:"email"` StripeConfig StripeConfig `mapstructure:"stripe" yaml:"stripe"` + OAuth2Config OAuth2Config `mapstructure:"oauth2" yaml:"oauth2"` IsDoneTickDotCom bool `mapstructure:"is_done_tick_dot_com" yaml:"is_done_tick_dot_com"` IsUserCreationDisabled bool `mapstructure:"is_user_creation_disabled" yaml:"is_user_creation_disabled"` } @@ -84,6 +86,17 @@ type EmailConfig struct { AppHost string `mapstructure:"appHost"` } +type OAuth2Config struct { + ClientID string `mapstructure:"client_id" yaml:"client_id"` + ClientSecret string `mapstructure:"client_secret" yaml:"client_secret"` + RedirectURL string `mapstructure:"redirect_url" yaml:"redirect_url"` + Scopes []string `mapstructure:"scopes" yaml:"scopes"` + AuthURL string `mapstructure:"auth_url" yaml:"auth_url"` + TokenURL string `mapstructure:"token_url" yaml:"token_url"` + UserInfoURL string `mapstructure:"user_info_url" yaml:"user_info_url"` + Name string `mapstructure:"name" yaml:"name"` +} + func NewConfig() *Config { return &Config{ Telegram: TelegramConfig{ @@ -126,9 +139,13 @@ func LoadConfig() *Config { } // get logger and log the current environment: fmt.Printf("--ConfigLoad config for environment: %s ", os.Getenv("DT_ENV")) + viper.SetEnvPrefix("DT") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() viper.AddConfigPath("./config") viper.SetConfigType("yaml") + err := viper.ReadInConfig() // print a useful error: if err != nil { @@ -141,8 +158,7 @@ func LoadConfig() *Config { panic(err) } fmt.Printf("--ConfigLoad name : %s ", config.Name) - viper.SetEnvPrefix("DT") - viper.AutomaticEnv() + configEnvironmentOverrides(&config) return &config diff --git a/config/local.yaml b/config/local.yaml index 7018f24..f9c888f 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -14,8 +14,8 @@ jwt: max_refresh: 168h server: port: 2021 - read_timeout: 2s - write_timeout: 1s + read_timeout: 10s + write_timeout: 10s rate_period: 60s rate_limit: 300 cors_allow_origins: diff --git a/config/selfhosted.env b/config/selfhosted.env index a03df9e..e5b6540 100644 --- a/config/selfhosted.env +++ b/config/selfhosted.env @@ -22,4 +22,10 @@ DT_EMAIL_HOST= DT_EMAIL_PORT= DT_EMAIL_KEY= DT_EMAIL_EMAIL= -DT_EMAIL_APP_HOST= \ No newline at end of file +DT_EMAIL_APP_HOST= +DT_OAUTH2_CLIENT_ID= +DT_OAUTH2_CLIENT_SECRET= +DT_OAUTH2_AUTH_URL= +DT_OAUTH2_TOKEN_URL= +DT_OAUTH2_USER_INFO_URL= +DT_OAUTH2_REDIRECT_URL= \ No newline at end of file diff --git a/config/selfhosted.yaml b/config/selfhosted.yaml index ca6c4af..26e2eb5 100644 --- a/config/selfhosted.yaml +++ b/config/selfhosted.yaml @@ -35,3 +35,11 @@ email: key: email: appHost: +oauth2: + client_id: + client_secret: + auth_url: + token_url: + user_info_url: + redirect_url: + name: \ No newline at end of file diff --git a/go.mod b/go.mod index ce42755..654b0c5 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/glebarez/sqlite v1.11.0 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/gregdel/pushover v1.3.1 github.com/rubenv/sql-migrate v1.7.0 github.com/spf13/viper v1.19.0 github.com/ulule/limiter/v3 v3.11.2 @@ -50,7 +52,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.4 // indirect - github.com/gregdel/pushover v1.3.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect diff --git a/go.sum b/go.sum index 92d35cc..7cd6d88 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= diff --git a/internal/authorization/identity_provider.go b/internal/authorization/identity_provider.go new file mode 100644 index 0000000..6714bfe --- /dev/null +++ b/internal/authorization/identity_provider.go @@ -0,0 +1,95 @@ +package auth + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + + "donetick.com/core/config" + "golang.org/x/oauth2" +) + +type IdentityProviderUserInfo struct { + Identifier string + DisplayName string + Email string +} + +type IdentityProvider struct { + config *config.OAuth2Config + isEnabled bool +} + +func NewIdentityProvider(cfg *config.Config) *IdentityProvider { + if cfg.OAuth2Config.ClientID == "" || cfg.OAuth2Config.ClientSecret == "" { + return &IdentityProvider{isEnabled: false} + } + return &IdentityProvider{config: &cfg.OAuth2Config, isEnabled: true} +} + +func (i *IdentityProvider) ExchangeToken(ctx context.Context, code string) (string, error) { + if !i.isEnabled { + return "", errors.New("identity provider is not enabled") + } + + conf := &oauth2.Config{ + ClientID: i.config.ClientID, + ClientSecret: i.config.ClientSecret, + RedirectURL: i.config.RedirectURL, + Scopes: i.config.Scopes, + Endpoint: oauth2.Endpoint{ + AuthURL: i.config.AuthURL, + TokenURL: i.config.TokenURL, + }, + } + token, err := conf.Exchange(ctx, code) + if err != nil { + return "", err + } + + accessToken, ok := token.AccessToken, token.Valid() + if !ok { + return "", errors.New("access token not found") + } + + return accessToken, nil +} + +func (i *IdentityProvider) GetUserInfo(ctx context.Context, accessToken string) (*IdentityProviderUserInfo, error) { + req, err := http.NewRequest("GET", i.config.UserInfoURL, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var claims map[string]any + err = json.Unmarshal(body, &claims) + if err != nil { + return nil, errors.New("failed to unmarshal claims") + } + userInfo := IdentityProviderUserInfo{} + if val, ok := claims["sub"]; ok { + userInfo.Identifier = val.(string) + } + if val, ok := claims["name"]; ok { + userInfo.DisplayName = val.(string) + } + if val, ok := claims["email"]; ok { + userInfo.Email = val.(string) + } + return &userInfo, nil +} diff --git a/internal/chore/handler.go b/internal/chore/handler.go index 159229b..5c39aa8 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -427,7 +427,7 @@ func (h *Handler) editChore(c *gin.Context) { }) return } - if currentUser.ID != oldChore.CreatedBy { + if !oldChore.CanEdit(currentUser.ID, circleUsers) { c.JSON(403, gin.H{ "error": "You are not allowed to edit this chore", }) @@ -998,7 +998,7 @@ func (h *Handler) completeChore(c *gin.Context) { } // confirm that the chore in completion window: if chore.CompletionWindow != nil { - if completedDate.After(chore.NextDueDate.Add(time.Hour * time.Duration(*chore.CompletionWindow))) { + if completedDate.Before(chore.NextDueDate.Add(time.Hour * time.Duration(*chore.CompletionWindow))) { c.JSON(400, gin.H{ "error": "Chore is out of completion window", }) diff --git a/internal/chore/model/model.go b/internal/chore/model/model.go index 2fa652b..0cff79b 100644 --- a/internal/chore/model/model.go +++ b/internal/chore/model/model.go @@ -3,6 +3,7 @@ package model import ( "time" + cModel "donetick.com/core/internal/circle/model" lModel "donetick.com/core/internal/label/model" tModel "donetick.com/core/internal/thing/model" thingModel "donetick.com/core/internal/thing/model" @@ -169,3 +170,15 @@ type ChoreReq struct { CompletionWindow *int `json:"completionWindow"` Description *string `json:"description"` } + +func (c *Chore) CanEdit(userID int, circleUsers []*cModel.UserCircleDetail) bool { + if c.CreatedBy == userID { + return true + } + for _, cu := range circleUsers { + if cu.UserID == userID && cu.Role == "admin" { + return true + } + } + return false +} diff --git a/internal/resource/handler.go b/internal/resource/handler.go new file mode 100644 index 0000000..c9eda6b --- /dev/null +++ b/internal/resource/handler.go @@ -0,0 +1,50 @@ +package resource + +import ( + "donetick.com/core/config" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "github.com/ulule/limiter/v3" +) + +type Resource struct { + Idp identityProvider `json:"identity_provider" binding:"omitempty"` +} +type identityProvider struct { + Auth_url string `json:"auth_url" binding:"omitempty"` + Client_ID string `json:"client_id" binding:"omitempty"` + Name string `json:"name" binding:"omitempty"` +} + +type Handler struct { + config config.Config +} + +func NewHandler(cfg *config.Config) *Handler { + return &Handler{ + config: *cfg, + } +} + +func (h *Handler) getResource(c *gin.Context) { + c.JSON(200, &Resource{ + Idp: identityProvider{ + Auth_url: h.config.OAuth2Config.AuthURL, + Client_ID: h.config.OAuth2Config.ClientID, + Name: h.config.OAuth2Config.Name, + }, + }) +} + +func Routes(r *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware, limiter *limiter.Limiter) { + resourceRoutes := r.Group("api/v1/resource") + + // skip resource endpoint for donetick.com + if h.config.IsDoneTickDotCom { + resourceRoutes.GET("", func(c *gin.Context) { + c.JSON(200, gin.H{}) + }) + return + } + resourceRoutes.GET("", h.getResource) +} diff --git a/internal/thing/webhook.go b/internal/thing/api.go similarity index 87% rename from internal/thing/webhook.go rename to internal/thing/api.go index 03498cb..65c4f20 100644 --- a/internal/thing/webhook.go +++ b/internal/thing/api.go @@ -16,7 +16,7 @@ import ( "github.com/gin-gonic/gin" ) -type Webhook struct { +type API struct { choreRepo *chRepo.ChoreRepository circleRepo *cRepo.CircleRepository thingRepo *tRepo.ThingRepository @@ -24,9 +24,9 @@ type Webhook struct { tRepo *tRepo.ThingRepository } -func NewWebhook(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, - thingRepo *tRepo.ThingRepository, userRepo *uRepo.UserRepository, tRepo *tRepo.ThingRepository) *Webhook { - return &Webhook{ +func NewAPI(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, + thingRepo *tRepo.ThingRepository, userRepo *uRepo.UserRepository, tRepo *tRepo.ThingRepository) *API { + return &API{ choreRepo: cr, circleRepo: circleRepo, thingRepo: thingRepo, @@ -35,7 +35,7 @@ func NewWebhook(cr *chRepo.ChoreRepository, circleRepo *cRepo.CircleRepository, } } -func (h *Webhook) UpdateThingState(c *gin.Context) { +func (h *API) UpdateThingState(c *gin.Context) { thing, shouldReturn := validateUserAndThing(c, h) if shouldReturn { return @@ -60,7 +60,7 @@ func (h *Webhook) UpdateThingState(c *gin.Context) { c.JSON(200, gin.H{}) } -func (h *Webhook) ChangeThingState(c *gin.Context) { +func (h *API) ChangeThingState(c *gin.Context) { thing, shouldReturn := validateUserAndThing(c, h) if shouldReturn { return @@ -109,7 +109,7 @@ func (h *Webhook) ChangeThingState(c *gin.Context) { c.JSON(200, gin.H{"state": thing.State}) } -func WebhookEvaluateTriggerAndScheduleDueDate(h *Webhook, c *gin.Context, thing *tModel.Thing) bool { +func WebhookEvaluateTriggerAndScheduleDueDate(h *API, c *gin.Context, thing *tModel.Thing) bool { // handler should be interface to not duplicate both WebhookEvaluateTriggerAndScheduleDueDate and EvaluateTriggerAndScheduleDueDate // this is bad code written Saturday at 2:25 AM @@ -134,7 +134,7 @@ func WebhookEvaluateTriggerAndScheduleDueDate(h *Webhook, c *gin.Context, thing return false } -func validateUserAndThing(c *gin.Context, h *Webhook) (*tModel.Thing, bool) { +func validateUserAndThing(c *gin.Context, h *API) (*tModel.Thing, bool) { apiToken := c.GetHeader("secretkey") if apiToken == "" { c.JSON(401, gin.H{"error": "Unauthorized"}) @@ -162,7 +162,7 @@ func validateUserAndThing(c *gin.Context, h *Webhook) (*tModel.Thing, bool) { return thing, false } -func Webhooks(cfg *config.Config, w *Webhook, r *gin.Engine, auth *jwt.GinJWTMiddleware) { +func APIs(cfg *config.Config, w *API, r *gin.Engine, auth *jwt.GinJWTMiddleware) { thingsAPI := r.Group("eapi/v1/things") diff --git a/internal/user/handler.go b/internal/user/handler.go index ed8a0b1..ed87890 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -31,16 +31,18 @@ type Handler struct { circleRepo *cRepo.CircleRepository jwtAuth *jwt.GinJWTMiddleware email *email.EmailSender + identityProvider *auth.IdentityProvider isDonetickDotCom bool IsUserCreationDisabled bool } -func NewHandler(ur *uRepo.UserRepository, cr *cRepo.CircleRepository, jwtAuth *jwt.GinJWTMiddleware, email *email.EmailSender, config *config.Config) *Handler { +func NewHandler(ur *uRepo.UserRepository, cr *cRepo.CircleRepository, jwtAuth *jwt.GinJWTMiddleware, email *email.EmailSender, idp *auth.IdentityProvider, config *config.Config) *Handler { return &Handler{ userRepo: ur, circleRepo: cr, jwtAuth: jwtAuth, email: email, + identityProvider: idp, isDonetickDotCom: config.IsDoneTickDotCom, IsUserCreationDisabled: config.IsUserCreationDisabled, } @@ -178,7 +180,8 @@ func (h *Handler) thirdPartyAuthCallback(c *gin.Context) { provider := c.Param("provider") logger.Infow("account.handler.thirdPartyAuthCallback", "provider", provider) - if provider == "google" { + switch provider { + case "google": c.Set("auth_provider", "3rdPartyAuth") type OAuthRequest struct { Token string `json:"token" binding:"required"` @@ -219,7 +222,7 @@ func (h *Handler) thirdPartyAuthCallback(c *gin.Context) { Image: userinfo.Picture, Password: encodedPassword, DisplayName: userinfo.GivenName, - Provider: 2, + Provider: uModel.AuthProviderGoogle, } createdUser, err := h.userRepo.CreateUser(c, acc) if err != nil { @@ -278,6 +281,105 @@ func (h *Handler) thirdPartyAuthCallback(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"token": tokenString, "expire": expire}) return + case "oauth2": + c.Set("auth_provider", "3rdPartyAuth") + // Read the ID token from the request bod + type Request struct { + Code string `json:"code"` + } + var req Request + if err := c.BindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + token, err := h.identityProvider.ExchangeToken(c, req.Code) + + if err != nil { + logger.Error("account.handler.thirdPartyAuthCallback (oauth2) failed to exchange token", "err", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"}) + return + } + + claims, err := h.identityProvider.GetUserInfo(c, token) + if err != nil { + logger.Error("account.handler.thirdPartyAuthCallback (oauth2) failed to get claims", "err", err) + } + + acc, err := h.userRepo.FindByEmail(c, claims.Email) + if err != nil { + // Create user + password := auth.GenerateRandomPassword(12) + encodedPassword, err := auth.EncodePassword(password) + if err != nil { + logger.Error("account.handler.thirdPartyAuthCallback (oauth2) password encoding failed", "err", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Password encoding failed"}) + return + } + acc = &uModel.User{ + Username: claims.Email, + Email: claims.Email, + Password: encodedPassword, + DisplayName: claims.DisplayName, + Provider: uModel.AuthProviderOAuth2, + } + createdUser, err := h.userRepo.CreateUser(c, acc) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Unable to create user", + }) + return + + } + // Create Circle for the user: + userCircle, err := h.circleRepo.CreateCircle(c, &cModel.Circle{ + Name: claims.DisplayName + "'s circle", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + InviteCode: utils.GenerateInviteCode(c), + }) + + if err != nil { + c.JSON(500, gin.H{ + "error": "Error creating circle", + }) + return + } + + if err := h.circleRepo.AddUserToCircle(c, &cModel.UserCircle{ + UserID: createdUser.ID, + CircleID: userCircle.ID, + Role: "admin", + IsActive: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }); err != nil { + c.JSON(500, gin.H{ + "error": "Error adding user to circle", + }) + return + } + createdUser.CircleID = userCircle.ID + if err := h.userRepo.UpdateUser(c, createdUser); err != nil { + c.JSON(500, gin.H{ + "error": "Error updating user", + }) + return + } + } + // ... (JWT generation and response) + c.Set("user_account", acc) + h.jwtAuth.Authenticator(c) + tokenString, expire, err := h.jwtAuth.TokenGenerator(acc) + if err != nil { + logger.Error("Unable to Generate a Token") + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Unable to Generate a Token", + }) + return + } + c.JSON(http.StatusOK, gin.H{"token": tokenString, "expire": expire}) + return } } @@ -610,5 +712,4 @@ func Routes(router *gin.Engine, h *Handler, auth *jwt.GinJWTMiddleware, limiter authRoutes.POST("reset", h.resetPassword) authRoutes.POST("password", h.updateUserPassword) } - } diff --git a/internal/user/model/model.go b/internal/user/model/model.go index 6a4c434..5d70e98 100644 --- a/internal/user/model/model.go +++ b/internal/user/model/model.go @@ -7,18 +7,18 @@ import ( ) type User struct { - ID int `json:"id" gorm:"primary_key"` // Unique identifier - DisplayName string `json:"displayName" gorm:"column:display_name"` // Display name - Username string `json:"username" gorm:"column:username;unique"` // Username (unique) - Email string `json:"email" gorm:"column:email;unique"` // Email (unique) - Provider int `json:"provider" gorm:"column:provider"` // Provider - Password string `json:"-" gorm:"column:password"` // Password - CircleID int `json:"circleID" gorm:"column:circle_id"` // Circle ID - ChatID int64 `json:"chatID" gorm:"column:chat_id"` // Telegram chat ID - Image string `json:"image" gorm:"column:image"` // Image - CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // Created at - UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` // Updated at - Disabled bool `json:"disabled" gorm:"column:disabled"` // Disabled + ID int `json:"id" gorm:"primary_key"` // Unique identifier + DisplayName string `json:"displayName" gorm:"column:display_name"` // Display name + Username string `json:"username" gorm:"column:username;unique"` // Username (unique) + Email string `json:"email" gorm:"column:email;unique"` // Email (unique) + Provider AuthProviderType `json:"provider" gorm:"column:provider"` // Provider + Password string `json:"-" gorm:"column:password"` // Password + CircleID int `json:"circleID" gorm:"column:circle_id"` // Circle ID + ChatID int64 `json:"chatID" gorm:"column:chat_id"` // Telegram chat ID + Image string `json:"image" gorm:"column:image"` // Image + CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` // Created at + UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` // Updated at + Disabled bool `json:"disabled" gorm:"column:disabled"` // Disabled // Email string `json:"email" gorm:"column:email"` // Email CustomerID *string `gorm:"column:customer_id;<-:false"` // read only column Subscription *string `json:"subscription" gorm:"column:subscription;<-:false"` // read only column @@ -48,3 +48,10 @@ type UserNotificationTarget struct { TargetID string `json:"target_id" gorm:"column:target_id"` // Target ID CreatedAt time.Time `json:"-" gorm:"column:created_at"` } +type AuthProviderType int + +const ( + AuthProviderDonetick AuthProviderType = iota + AuthProviderOAuth2 + AuthProviderGoogle +) diff --git a/main.go b/main.go index 3772ebd..a168f39 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ import ( "donetick.com/core/internal/email" label "donetick.com/core/internal/label" lRepo "donetick.com/core/internal/label/repo" + "donetick.com/core/internal/resource" notifier "donetick.com/core/internal/notifier" nRepo "donetick.com/core/internal/notifier/repo" @@ -52,6 +53,8 @@ func main() { // fx.Provide(config.NewConfig), fx.Provide(auth.NewAuthMiddleware), + fx.Provide(auth.NewIdentityProvider), + fx.Provide(resource.NewHandler), // fx.Provide(NewBot), fx.Provide(database.NewDatabase), @@ -89,7 +92,7 @@ func main() { fx.Provide(lRepo.NewLabelRepository), fx.Provide(label.NewHandler), - fx.Provide(thing.NewWebhook), + fx.Provide(thing.NewAPI), fx.Provide(thing.NewHandler), fx.Provide(chore.NewAPI), @@ -103,9 +106,10 @@ func main() { user.Routes, circle.Routes, thing.Routes, - thing.Webhooks, + thing.APIs, label.Routes, frontend.Routes, + resource.Routes, func(r *gin.Engine) {}, ),