package cmd import ( "fmt" "net/http" "path/filepath" "time" "github.com/gin-contrib/cache" "github.com/gin-contrib/cache/persistence" "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" "github.com/urfave/cli/v3" "github.com/mayswind/ezbookkeeping/pkg/api" "github.com/mayswind/ezbookkeeping/pkg/auth/oauth2" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/cron" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/mcp" "github.com/mayswind/ezbookkeeping/pkg/middlewares" "github.com/mayswind/ezbookkeeping/pkg/requestid" "github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/validators" ) // WebServer represents the server command var WebServer = &cli.Command{ Name: "server", Usage: "ezBookkeeping web server operation", Commands: []*cli.Command{ { Name: "run", Usage: "Run ezBookkeeping web server", Action: bindAction(startWebServer), }, }, } func startWebServer(c *core.CliContext) error { config, err := initializeSystem(c) if err != nil { return err } log.BootInfof(c, "[webserver.startWebServer] static root path is %s", config.StaticRootPath) if config.AutoUpdateDatabase { err = updateAllDatabaseTablesStructure(c) if err != nil { log.BootErrorf(c, "[webserver.startWebServer] update database table structure failed, because %s", err.Error()) return err } } err = requestid.InitializeRequestIdGenerator(c, config) if err != nil { log.BootErrorf(c, "[webserver.startWebServer] initializes requestid generator failed, because %s", err.Error()) return err } err = mcp.InitializeMCPHandlers(config) if err != nil { log.BootErrorf(c, "[webserver.startWebServer] initializes mcp handlers failed, because %s", err.Error()) return err } err = oauth2.InitializeOAuth2Provider(config) if err != nil { log.BootErrorf(c, "[webserver.startWebServer] initializes oauth 2.0 provider failed, because %s", err.Error()) return err } err = cron.InitializeCronJobSchedulerContainer(c, config, true) if err != nil { log.BootErrorf(c, "[webserver.startWebServer] initializes cron job scheduler failed, because %s", err.Error()) return err } serverInfo := fmt.Sprintf("current server id is %d, current instance id is %d", requestid.Container.GetCurrentServerUniqId(), requestid.Container.GetCurrentInstanceUniqId()) uuidServerInfo := "" if config.UuidGeneratorType == settings.InternalUuidGeneratorType { uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId) } log.BootInfof(c, "[webserver.startWebServer] %s%s", serverInfo, uuidServerInfo) if config.Mode == settings.MODE_PRODUCTION { gin.SetMode(gin.ReleaseMode) } workboxFileNames := utils.ListFileNamesWithPrefixAndSuffix(config.StaticRootPath, "workbox-", ".js") router := gin.New() router.Use(bindMiddleware(middlewares.Recovery)) if config.EnableGZip { router.Use(gzip.Gzip(gzip.DefaultCompression)) } if v, ok := binding.Validator.Engine().(*validator.Validate); ok { _ = v.RegisterValidation("notBlank", validators.NotBlank) _ = v.RegisterValidation("validUsername", validators.ValidUsername) _ = v.RegisterValidation("validEmail", validators.ValidEmail) _ = v.RegisterValidation("validNickname", validators.ValidNickname) _ = v.RegisterValidation("validCurrency", validators.ValidCurrency) _ = v.RegisterValidation("validHexRGBColor", validators.ValidHexRGBColor) _ = v.RegisterValidation("validAmountFilter", validators.ValidAmountFilter) _ = v.RegisterValidation("validFiscalYearStart", validators.ValidateFiscalYearStart) } router.NoRoute(bindApi(api.Default.ApiNotFound)) router.NoMethod(bindApi(api.Default.MethodNotAllowed)) serverSettingsCacheStore := persistence.NewInMemoryStore(time.Minute) router.StaticFile("/", filepath.Join(config.StaticRootPath, "index.html")) router.Static("/js", filepath.Join(config.StaticRootPath, "js")) router.Static("/css", filepath.Join(config.StaticRootPath, "css")) router.Static("/img", filepath.Join(config.StaticRootPath, "img")) router.Static("/fonts", filepath.Join(config.StaticRootPath, "fonts")) router.StaticFile("robots.txt", filepath.Join(config.StaticRootPath, "robots.txt")) router.StaticFile("favicon.ico", filepath.Join(config.StaticRootPath, "favicon.ico")) router.StaticFile("favicon.png", filepath.Join(config.StaticRootPath, "favicon.png")) router.StaticFile("touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png")) router.StaticFile("manifest.json", filepath.Join(config.StaticRootPath, "manifest.json")) router.StaticFile("sw.js", filepath.Join(config.StaticRootPath, "sw.js")) router.GET("/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore)) for i := 0; i < len(workboxFileNames); i++ { router.StaticFile("/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i])) } router.StaticFile("/mobile", filepath.Join(config.StaticRootPath, "mobile.html")) router.Static("/mobile/js", filepath.Join(config.StaticRootPath, "js")) router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css")) router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img")) router.Static("/mobile/fonts", filepath.Join(config.StaticRootPath, "fonts")) router.StaticFile("/mobile/favicon.ico", filepath.Join(config.StaticRootPath, "favicon.ico")) router.StaticFile("/mobile/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png")) router.StaticFile("/mobile/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png")) router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json")) router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js")) router.GET("/mobile/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore)) for i := 0; i < len(workboxFileNames); i++ { router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i])) } router.StaticFile("/desktop", filepath.Join(config.StaticRootPath, "desktop.html")) router.Static("/desktop/js", filepath.Join(config.StaticRootPath, "js")) router.Static("/desktop/css", filepath.Join(config.StaticRootPath, "css")) router.Static("/desktop/img", filepath.Join(config.StaticRootPath, "img")) router.Static("/desktop/fonts", filepath.Join(config.StaticRootPath, "fonts")) router.StaticFile("/desktop/favicon.ico", filepath.Join(config.StaticRootPath, "favicon.ico")) router.StaticFile("/desktop/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png")) router.StaticFile("/desktop/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png")) router.StaticFile("/desktop/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json")) router.StaticFile("/desktop/sw.js", filepath.Join(config.StaticRootPath, "sw.js")) router.GET("/desktop/server_settings.js", bindCachedJs(api.ServerSettings.ServerSettingsJavascriptHandler, serverSettingsCacheStore)) for i := 0; i < len(workboxFileNames); i++ { router.StaticFile("/desktop/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i])) } if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL { avatarRoute := router.Group("/avatar") avatarRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString)) { avatarRoute.GET("/:fileName", bindImage(api.Users.UserGetAvatarHandler)) } } if config.EnableTransactionPictures { pictureRoute := router.Group("/pictures") pictureRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString)) { pictureRoute.GET("/:fileName", bindImage(api.TransactionPictures.TransactionPictureGetHandler)) } } router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler)) proxyRoute := router.Group("/proxy") proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString)) { if config.EnableMapDataFetchProxy { if config.MapProvider == settings.OpenStreetMapProvider || config.MapProvider == settings.OpenStreetMapHumanitarianStyleProvider || config.MapProvider == settings.OpenTopoMapProvider || config.MapProvider == settings.OPNVKarteMapProvider || config.MapProvider == settings.CyclOSMMapProvider || config.MapProvider == settings.CartoDBMapProvider || config.MapProvider == settings.TomTomMapProvider || config.MapProvider == settings.TianDiTuProvider || config.MapProvider == settings.CustomProvider { proxyRoute.GET("/map/tile/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapTileImageProxyHandler)) } if config.MapProvider == settings.TianDiTuProvider || (config.MapProvider == settings.CustomProvider && config.CustomMapTileServerAnnotationLayerUrl != "") { proxyRoute.GET("/map/annotation/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.MapAnnotationImageProxyHandler)) } } } if config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod { amapApiProxyRoute := router.Group("/_AMapService") amapApiProxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByCookie)) { amapApiProxyRoute.GET("/*action", bindProxy(api.AmapApis.AmapApiProxyHandler)) } } qrCodeRoute := router.Group("/qrcode") qrCodeRoute.Use(bindMiddleware(middlewares.RequestId(config))) { qrCodeCacheStore := persistence.NewInMemoryStore(time.Minute) qrCodeRoute.GET("/mobile_url.png", bindCachedImage(api.QrCodes.MobileUrlQrCodeHandler, qrCodeCacheStore)) } if config.EnableMCPServer { mcpRoute := router.Group("/mcp") mcpRoute.Use(bindMiddleware(middlewares.RequestId(config))) mcpRoute.Use(bindMiddleware(middlewares.RequestLog)) mcpRoute.Use(bindMiddleware(middlewares.MCPServerIpLimit(config))) mcpRoute.Use(bindMiddleware(middlewares.JWTMCPAuthorization)) { mcpRoute.POST("", bindJSONRPCApi(map[string]core.JSONRPCApiHandlerFunc{ "initialize": api.ModelContextProtocols.InitializeHandler, "resources/list": api.ModelContextProtocols.ListResourcesHandler, "resources/read": api.ModelContextProtocols.ReadResourceHandler, "tools/list": api.ModelContextProtocols.ListToolsHandler, "tools/call": api.ModelContextProtocols.CallToolHandler, "ping": api.ModelContextProtocols.PingHandler, }, map[string]int{ "notifications/initialized": http.StatusAccepted, })) mcpRoute.GET("", bindApi(api.Default.MethodNotAllowed)) } } if config.EnableOAuth2Login { oauth2Route := router.Group("/oauth2") oauth2Route.Use(bindMiddleware(middlewares.RequestId(config))) oauth2Route.Use(bindMiddleware(middlewares.RequestLog)) { oauth2Route.GET("/login", bindRedirect(api.OAuth2Authentications.LoginHandler)) oauth2Route.GET("/callback", bindRedirect(api.OAuth2Authentications.CallbackHandler)) } } apiRoute := router.Group("/api") apiRoute.Use(bindMiddleware(middlewares.RequestId(config))) apiRoute.Use(bindMiddleware(middlewares.RequestLog)) { if config.EnableInternalAuth { apiRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.AuthorizeHandler, config)) } if config.EnableInternalAuth && config.EnableTwoFactor { twoFactorRoute := apiRoute.Group("/2fa") twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization)) { twoFactorRoute.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeHandler, config)) twoFactorRoute.POST("/recovery.json", bindApiWithTokenUpdate(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler, config)) } } if config.EnableOAuth2Login { oauth2Route := apiRoute.Group("/oauth2") oauth2Route.Use(bindMiddleware(middlewares.JWTOAuth2CallbackAuthorization)) { oauth2Route.POST("/authorize.json", bindApiWithTokenUpdate(api.Authorizations.OAuth2CallbackAuthorizeHandler, config)) } } if config.EnableInternalAuth && config.EnableUserRegister { apiRoute.POST("/register.json", bindApiWithTokenUpdate(api.Users.UserRegisterHandler, config)) } if config.EnableUserVerifyEmail { apiRoute.POST("/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByUnloginUserHandler)) emailVerifyRoute := apiRoute.Group("/verify_email") emailVerifyRoute.Use(bindMiddleware(middlewares.JWTEmailVerifyAuthorization)) { emailVerifyRoute.POST("/by_token.json", bindApi(api.Users.UserEmailVerifyHandler)) } } if config.EnableInternalAuth && config.EnableUserForgetPassword { apiRoute.POST("/forget_password/request.json", bindApi(api.ForgetPasswords.UserForgetPasswordRequestHandler)) resetPasswordRoute := apiRoute.Group("/forget_password/reset") resetPasswordRoute.Use(bindMiddleware(middlewares.JWTResetPasswordAuthorization)) { resetPasswordRoute.POST("/by_token.json", bindApi(api.ForgetPasswords.UserResetPasswordHandler)) } } apiRoute.GET("/logout.json", bindApiWithTokenUpdate(api.Tokens.TokenRevokeCurrentHandler, config)) apiV1Route := apiRoute.Group("/v1") apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization)) { // Tokens apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler)) apiV1Route.POST("/tokens/generate/mcp.json", bindApi(api.Tokens.TokenGenerateMCPHandler)) apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler)) apiV1Route.POST("/tokens/revoke_all.json", bindApi(api.Tokens.TokenRevokeAllHandler)) apiV1Route.POST("/tokens/refresh.json", bindApiWithTokenUpdate(api.Tokens.TokenRefreshHandler, config)) // Users apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler)) apiV1Route.POST("/users/profile/update.json", bindApiWithTokenUpdate(api.Users.UserUpdateProfileHandler, config)) if config.AvatarProvider == core.USER_AVATAR_PROVIDER_INTERNAL { apiV1Route.POST("/users/avatar/update.json", bindApi(api.Users.UserUpdateAvatarHandler)) apiV1Route.POST("/users/avatar/remove.json", bindApi(api.Users.UserRemoveAvatarHandler)) } if config.EnableUserVerifyEmail { apiV1Route.POST("/users/verify_email/resend.json", bindApi(api.Users.UserSendVerifyEmailByLoginedUserHandler)) } // Application Cloud Settings apiV1Route.GET("/users/settings/cloud/get.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsGetHandler)) apiV1Route.POST("/users/settings/cloud/update.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsUpdateHandler)) apiV1Route.POST("/users/settings/cloud/disable.json", bindApi(api.UserApplicationCloudSettings.ApplicationSettingsDisableHandler)) // Two-Factor Authorization if config.EnableTwoFactor { apiV1Route.GET("/users/2fa/status.json", bindApi(api.TwoFactorAuthorizations.TwoFactorStatusHandler)) apiV1Route.POST("/users/2fa/enable/request.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableRequestHandler)) apiV1Route.POST("/users/2fa/enable/confirm.json", bindApiWithTokenUpdate(api.TwoFactorAuthorizations.TwoFactorEnableConfirmHandler, config)) apiV1Route.POST("/users/2fa/disable.json", bindApi(api.TwoFactorAuthorizations.TwoFactorDisableHandler)) apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler)) } // Data apiV1Route.GET("/data/statistics.json", bindApi(api.DataManagements.DataStatisticsHandler)) apiV1Route.POST("/data/clear/all.json", bindApi(api.DataManagements.ClearAllDataHandler)) apiV1Route.POST("/data/clear/transactions.json", bindApi(api.DataManagements.ClearAllTransactionsHandler)) apiV1Route.POST("/data/clear/transactions/by_account.json", bindApi(api.DataManagements.ClearAllTransactionsByAccountHandler)) if config.EnableDataExport { apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataToEzbookkeepingCSVHandler)) apiV1Route.GET("/data/export.tsv", bindTsv(api.DataManagements.ExportDataToEzbookkeepingTSVHandler)) } // Accounts apiV1Route.GET("/accounts/list.json", bindApi(api.Accounts.AccountListHandler)) apiV1Route.GET("/accounts/get.json", bindApi(api.Accounts.AccountGetHandler)) apiV1Route.POST("/accounts/add.json", bindApi(api.Accounts.AccountCreateHandler)) apiV1Route.POST("/accounts/modify.json", bindApi(api.Accounts.AccountModifyHandler)) apiV1Route.POST("/accounts/hide.json", bindApi(api.Accounts.AccountHideHandler)) apiV1Route.POST("/accounts/move.json", bindApi(api.Accounts.AccountMoveHandler)) apiV1Route.POST("/accounts/delete.json", bindApi(api.Accounts.AccountDeleteHandler)) apiV1Route.POST("/accounts/sub_account/delete.json", bindApi(api.Accounts.SubAccountDeleteHandler)) // Transactions apiV1Route.GET("/transactions/count.json", bindApi(api.Transactions.TransactionCountHandler)) apiV1Route.GET("/transactions/list.json", bindApi(api.Transactions.TransactionListHandler)) apiV1Route.GET("/transactions/list/by_month.json", bindApi(api.Transactions.TransactionMonthListHandler)) apiV1Route.GET("/transactions/reconciliation_statements.json", bindApi(api.Transactions.TransactionReconciliationStatementHandler)) apiV1Route.GET("/transactions/statistics.json", bindApi(api.Transactions.TransactionStatisticsHandler)) apiV1Route.GET("/transactions/statistics/trends.json", bindApi(api.Transactions.TransactionStatisticsTrendsHandler)) apiV1Route.GET("/transactions/amounts.json", bindApi(api.Transactions.TransactionAmountsHandler)) apiV1Route.GET("/transactions/get.json", bindApi(api.Transactions.TransactionGetHandler)) apiV1Route.POST("/transactions/add.json", bindApi(api.Transactions.TransactionCreateHandler)) apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler)) apiV1Route.POST("/transactions/move/all.json", bindApi(api.Transactions.TransactionMoveAllBetweenAccountsHandler)) apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler)) if config.EnableDataImport { apiV1Route.POST("/transactions/parse_dsv_file.json", bindApi(api.Transactions.TransactionParseImportDsvFileDataHandler)) apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler)) apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler)) apiV1Route.GET("/transactions/import/process.json", bindApi(api.Transactions.TransactionImportProcessHandler)) } // Transaction Pictures if config.EnableTransactionPictures { apiV1Route.POST("/transaction/pictures/upload.json", bindApi(api.TransactionPictures.TransactionPictureUploadHandler)) apiV1Route.POST("/transaction/pictures/remove_unused.json", bindApi(api.TransactionPictures.TransactionPictureRemoveUnusedHandler)) } // Transaction Categories apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler)) apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler)) apiV1Route.POST("/transaction/categories/add.json", bindApi(api.TransactionCategories.CategoryCreateHandler)) apiV1Route.POST("/transaction/categories/add_batch.json", bindApi(api.TransactionCategories.CategoryCreateBatchHandler)) apiV1Route.POST("/transaction/categories/modify.json", bindApi(api.TransactionCategories.CategoryModifyHandler)) apiV1Route.POST("/transaction/categories/hide.json", bindApi(api.TransactionCategories.CategoryHideHandler)) apiV1Route.POST("/transaction/categories/move.json", bindApi(api.TransactionCategories.CategoryMoveHandler)) apiV1Route.POST("/transaction/categories/delete.json", bindApi(api.TransactionCategories.CategoryDeleteHandler)) // Transaction Tags apiV1Route.GET("/transaction/tags/list.json", bindApi(api.TransactionTags.TagListHandler)) apiV1Route.GET("/transaction/tags/get.json", bindApi(api.TransactionTags.TagGetHandler)) apiV1Route.POST("/transaction/tags/add.json", bindApi(api.TransactionTags.TagCreateHandler)) apiV1Route.POST("/transaction/tags/add_batch.json", bindApi(api.TransactionTags.TagCreateBatchHandler)) apiV1Route.POST("/transaction/tags/modify.json", bindApi(api.TransactionTags.TagModifyHandler)) apiV1Route.POST("/transaction/tags/hide.json", bindApi(api.TransactionTags.TagHideHandler)) apiV1Route.POST("/transaction/tags/move.json", bindApi(api.TransactionTags.TagMoveHandler)) apiV1Route.POST("/transaction/tags/delete.json", bindApi(api.TransactionTags.TagDeleteHandler)) // Transaction Templates apiV1Route.GET("/transaction/templates/list.json", bindApi(api.TransactionTemplates.TemplateListHandler)) apiV1Route.GET("/transaction/templates/get.json", bindApi(api.TransactionTemplates.TemplateGetHandler)) apiV1Route.POST("/transaction/templates/add.json", bindApi(api.TransactionTemplates.TemplateCreateHandler)) apiV1Route.POST("/transaction/templates/modify.json", bindApi(api.TransactionTemplates.TemplateModifyHandler)) apiV1Route.POST("/transaction/templates/hide.json", bindApi(api.TransactionTemplates.TemplateHideHandler)) apiV1Route.POST("/transaction/templates/move.json", bindApi(api.TransactionTemplates.TemplateMoveHandler)) apiV1Route.POST("/transaction/templates/delete.json", bindApi(api.TransactionTemplates.TemplateDeleteHandler)) // Large Language Models if config.ReceiptImageRecognitionLLMConfig != nil && config.ReceiptImageRecognitionLLMConfig.LLMProvider != "" { if config.TransactionFromAIImageRecognition { apiV1Route.POST("/llm/transactions/recognize_receipt_image.json", bindApi(api.LargeLanguageModels.RecognizeReceiptImageHandler)) } } // Exchange Rates apiV1Route.GET("/exchange_rates/latest.json", bindApi(api.ExchangeRates.LatestExchangeRateHandler)) apiV1Route.POST("/exchange_rates/user_custom/update.json", bindApi(api.ExchangeRates.UserCustomExchangeRateUpdateHandler)) apiV1Route.POST("/exchange_rates/user_custom/delete.json", bindApi(api.ExchangeRates.UserCustomExchangeRateDeleteHandler)) // System apiV1Route.GET("/systems/version.json", bindApi(api.Systems.VersionHandler)) } } listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort) if config.Protocol == settings.SCHEME_SOCKET { log.BootInfof(c, "[webserver.startWebServer] will run at socks:%s", config.UnixSocketPath) err = router.RunUnix(config.UnixSocketPath) } else if config.Protocol == settings.SCHEME_HTTP { log.BootInfof(c, "[webserver.startWebServer] will run at http://%s", listenAddr) err = router.Run(listenAddr) } else if config.Protocol == settings.SCHEME_HTTPS { log.BootInfof(c, "[webserver.startWebServer] will run at https://%s", listenAddr) err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile) } else { err = errs.ErrInvalidProtocol } if err != nil { log.BootErrorf(c, "[webserver.startWebServer] cannot start, because %s", err) return err } return nil } func bindMiddleware(fn core.MiddlewareHandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { fn(core.WrapWebContext(c)) } } func bindRedirect(fn core.RedirectHandlerFunc) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) url, err := fn(c) if err != nil { utils.PrintJsonErrorResult(c, err) } else { c.Redirect(http.StatusFound, url) } } } func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) result, err := fn(c) if err != nil { utils.PrintJsonErrorResult(c, err) } else { utils.PrintJsonSuccessResult(c, result) } } } func bindApiWithTokenUpdate(fn core.ApiHandlerFunc, config *settings.Config) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) result, err := fn(c) if err == nil && config.MapProvider == settings.AmapProvider && config.AmapSecurityVerificationMethod == settings.AmapSecurityVerificationInternalProxyMethod { middlewares.AmapApiProxyAuthCookie(c, config) } if err != nil { utils.PrintJsonErrorResult(c, err) } else { utils.PrintJsonSuccessResult(c, result) } } } func bindJSONRPCApi(fns map[string]core.JSONRPCApiHandlerFunc, skipMethods map[string]int) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) var jsonRPCRequest core.JSONRPCRequest reqErr := c.ShouldBindBodyWithJSON(&jsonRPCRequest) if reqErr != nil { utils.PrintJSONRPCErrorResult(c, nil, errs.NewIncompleteOrIncorrectSubmissionError(reqErr)) return } if skipMethods != nil { httpStatusCode, exists := skipMethods[jsonRPCRequest.Method] if exists { c.AbortWithStatus(httpStatusCode) return } } fn, exists := fns[jsonRPCRequest.Method] if !exists { utils.PrintJSONRPCErrorResult(c, &jsonRPCRequest, errs.ErrApiNotFound) return } result, err := fn(c, &jsonRPCRequest) if err != nil { utils.PrintJSONRPCErrorResult(c, &jsonRPCRequest, err) } else { utils.PrintJSONRPCSuccessResult(c, &jsonRPCRequest, result) } } } func bindEventStreamApi(fn core.EventStreamApiHandlerFunc) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) utils.SetEventStreamHeader(c) err := fn(c) if err != nil { utils.WriteEventStreamJsonErrorResult(c, err) } } } func bindCachedJs(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc { return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) result, _, err := fn(c) if err != nil { utils.PrintDataErrorResult(c, "text/javascript", err) } else { utils.PrintDataSuccessResult(c, "text/javascript; charset=utf-8", "", result) } }) } func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) result, fileName, err := fn(c) if err != nil { utils.PrintDataErrorResult(c, "text/text", err) } else { utils.PrintDataSuccessResult(c, "text/csv; charset=utf-8", fileName, result) } } } func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) result, fileName, err := fn(c) if err != nil { utils.PrintDataErrorResult(c, "text/text", err) } else { utils.PrintDataSuccessResult(c, "text/tab-separated-values; charset=utf-8", fileName, result) } } } func bindImage(fn core.ImageHandlerFunc) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) result, contentType, err := fn(c) if err != nil { utils.PrintDataErrorResult(c, "text/text", err) } else { utils.PrintDataSuccessResult(c, contentType, "", result) } } } func bindCachedImage(fn core.ImageHandlerFunc, store persistence.CacheStore) gin.HandlerFunc { return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) result, contentType, err := fn(c) if err != nil { utils.PrintDataErrorResult(c, "text/text", err) } else { utils.PrintDataSuccessResult(c, contentType, "", result) } }) } func bindProxy(fn core.ProxyHandlerFunc) gin.HandlerFunc { return func(ginCtx *gin.Context) { c := core.WrapWebContext(ginCtx) proxy, err := fn(c) if err != nil { utils.PrintDataErrorResult(c, "text/text", err) } else { proxy.ServeHTTP(c.Writer, c.Request) } } }