diff --git a/cmd/webserver.go b/cmd/webserver.go
index f104304a..15fb59b4 100644
--- a/cmd/webserver.go
+++ b/cmd/webserver.go
@@ -137,6 +137,12 @@ func startWebServer(c *cli.Context) error {
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
+ proxyRoute := router.Group("/proxy")
+ proxyRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString))
+ {
+ proxyRoute.GET("/openstreetmap/tile/:zoomLevel/:coordinateX/:fileName", bindProxy(api.MapImages.OpenStreetMapTileImageProxyHandler))
+ }
+
apiRoute := router.Group("/api")
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
@@ -288,3 +294,16 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc {
}
}
}
+
+func bindProxy(fn core.ProxyHandlerFunc) gin.HandlerFunc {
+ return func(ginCtx *gin.Context) {
+ c := core.WrapContext(ginCtx)
+ proxy, err := fn(c)
+
+ if err != nil {
+ utils.PrintDataErrorResult(c, "text/text", err)
+ } else {
+ proxy.ServeHTTP(c.Writer, c.Request)
+ }
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 5df74482..f588aa8d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.0.3",
"js-cookie": "^3.0.1",
+ "leaflet": "^1.9.3",
"line-awesome": "^1.3.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
@@ -5200,6 +5201,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/leaflet": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz",
+ "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ=="
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
diff --git a/package.json b/package.json
index 57a607ef..dd7e7e3c 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"framework7-icons": "^5.0.5",
"framework7-vue": "^8.0.3",
"js-cookie": "^3.0.1",
+ "leaflet": "^1.9.3",
"line-awesome": "^1.3.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
diff --git a/pkg/api/map_image_proxies.go b/pkg/api/map_image_proxies.go
new file mode 100644
index 00000000..0c31feb2
--- /dev/null
+++ b/pkg/api/map_image_proxies.go
@@ -0,0 +1,41 @@
+package api
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+
+ "github.com/mayswind/ezbookkeeping/pkg/core"
+ "github.com/mayswind/ezbookkeeping/pkg/errs"
+)
+
+const openStreetMapTileImageUrlFormat = "https://tile.openstreetmap.org/%s/%s/%s" // https://tile.openstreetmap.org/{z}/{x}/{y}.png
+
+// MapImageProxy represents map image proxy
+type MapImageProxy struct {
+}
+
+// Initialize a map image proxy singleton instance
+var (
+ MapImages = &MapImageProxy{}
+)
+
+// OpenStreetMapTileImageProxyHandler returns open street map tile image
+func (p *MapImageProxy) OpenStreetMapTileImageProxyHandler(c *core.Context) (*httputil.ReverseProxy, *errs.Error) {
+ director := func(req *http.Request) {
+ zoomLevel := c.Param("zoomLevel")
+ coordinateX := c.Param("coordinateX")
+ fileName := c.Param("fileName")
+
+ imageRawUrl := fmt.Sprintf(openStreetMapTileImageUrlFormat, zoomLevel, coordinateX, fileName)
+ imageUrl, _ := url.Parse(imageRawUrl)
+
+ req.Header.Del("Authorization")
+ req.URL = imageUrl
+ req.RequestURI = req.URL.RequestURI()
+ req.Host = imageUrl.Host
+ }
+
+ return &httputil.ReverseProxy{Director: director}, nil
+}
diff --git a/pkg/core/handler.go b/pkg/core/handler.go
index e6804c04..9a88140a 100644
--- a/pkg/core/handler.go
+++ b/pkg/core/handler.go
@@ -1,6 +1,10 @@
package core
-import "github.com/mayswind/ezbookkeeping/pkg/errs"
+import (
+ "net/http/httputil"
+
+ "github.com/mayswind/ezbookkeeping/pkg/errs"
+)
// MiddlewareHandlerFunc represents the middleware handler function
type MiddlewareHandlerFunc func(*Context)
@@ -10,3 +14,6 @@ type ApiHandlerFunc func(*Context) (interface{}, *errs.Error)
// DataHandlerFunc represents the handler function that returns byte array
type DataHandlerFunc func(*Context) ([]byte, string, *errs.Error)
+
+// ProxyHandlerFunc represents the reverse proxy handler function
+type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
diff --git a/pkg/middlewares/authorization.go b/pkg/middlewares/authorization.go
index 6d5e429f..6161bc95 100644
--- a/pkg/middlewares/authorization.go
+++ b/pkg/middlewares/authorization.go
@@ -37,6 +37,21 @@ func JWTAuthorization(c *core.Context) {
c.Next()
}
+// JWTAuthorizationByQueryString verifies whether current request is valid by jwt token
+func JWTAuthorizationByQueryString(c *core.Context) {
+ token, exists := c.GetQuery(tokenQueryStringParam)
+
+ if !exists {
+ log.WarnfWithRequestId(c, "[authorization.JWTAuthorizationByQueryString] no token provided")
+ utils.PrintJsonErrorResult(c, errs.ErrUnauthorizedAccess)
+ return
+ }
+
+ c.Request.Header.Set("Authorization", token)
+
+ JWTAuthorization(c)
+}
+
// JWTTwoFactorAuthorization verifies whether current request is valid by 2fa passcode
func JWTTwoFactorAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
diff --git a/src/components/mobile/MapSheet.vue b/src/components/mobile/MapSheet.vue
new file mode 100644
index 00000000..f788518e
--- /dev/null
+++ b/src/components/mobile/MapSheet.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/services.js b/src/lib/services.js
index bad91bab..8b445a7a 100644
--- a/src/lib/services.js
+++ b/src/lib/services.js
@@ -389,4 +389,9 @@ export default {
ignoreError: !!ignoreError
});
},
+ generateOpenStreetMapTileImageUrl: () => {
+ const token = userState.getToken();
+
+ return 'proxy/openstreetmap/tile/{z}/{x}/{y}.png?token=' + token;
+ },
};
diff --git a/src/locales/en.js b/src/locales/en.js
index bf87ee50..36716f81 100644
--- a/src/locales/en.js
+++ b/src/locales/en.js
@@ -703,6 +703,8 @@ export default {
'Back': 'Back',
'Load More': 'Load More',
'No data': 'No data',
+ 'Zoom in': 'Zoom in',
+ 'Zoom out': 'Zoom out',
'Change Language': 'Change Language',
'Date is too early': 'Date is too early',
'Unlock Application': 'Unlock Application',
@@ -833,6 +835,7 @@ export default {
'Geographic Location': 'Geographic Location',
'No Location': 'No Location',
'Getting Location...': 'Getting Location...',
+ 'Show on the map': 'Show on the map',
'Update Geographic Location': 'Update Geographic Location',
'Clear Geographic Location': 'Clear Geographic Location',
'Unable to get current position': 'Unable to get current position',
diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js
index 1a78a470..bda8f3c2 100644
--- a/src/locales/zh_Hans.js
+++ b/src/locales/zh_Hans.js
@@ -703,6 +703,8 @@ export default {
'Back': '返回',
'Load More': '加载更多',
'No data': '没有数据',
+ 'Zoom in': '放大',
+ 'Zoom out': '缩小',
'Change Language': '修改语言',
'Date is too early': '日期过早',
'Unlock Application': '解锁应用',
@@ -833,6 +835,7 @@ export default {
'Geographic Location': '地理位置',
'No Location': '没有位置',
'Getting Location...': '正在获取位置...',
+ 'Show on the map': '在地图上显示',
'Update Geographic Location': '更新地理位置',
'Clear Geographic Location': '清除地理位置',
'Unable to get current position': '无法获取当前地理位置',
diff --git a/src/mobile-main.js b/src/mobile-main.js
index ff8daaef..ff75c29c 100644
--- a/src/mobile-main.js
+++ b/src/mobile-main.js
@@ -73,6 +73,9 @@ import 'line-awesome/dist/line-awesome/css/line-awesome.css';
import VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
+import * as Leaflet from 'leaflet/dist/leaflet-src.esm.js';
+import 'leaflet/dist/leaflet.css';
+
import api from './consts/api.js';
import datetime from './consts/datetime.js';
import currency from './consts/currency.js';
@@ -135,6 +138,7 @@ import IconSelectionSheet from './components/mobile/IconSelectionSheet.vue';
import ColorSelectionSheet from './components/mobile/ColorSelectionSheet.vue';
import InformationSheet from './components/mobile/InformationSheet.vue';
import NumberPadSheet from './components/mobile/NumberPadSheet.vue';
+import MapSheet from './components/mobile/MapSheet.vue';
import TransactionTagSelectionSheet from './components/mobile/TransactionTagSelectionSheet.vue';
import TextareaAutoSize from "./directives/mobile/textareaAutoSize.js";
@@ -254,6 +258,7 @@ app.component('IconSelectionSheet', IconSelectionSheet);
app.component('ColorSelectionSheet', ColorSelectionSheet);
app.component('InformationSheet', InformationSheet);
app.component('NumberPadSheet', NumberPadSheet);
+app.component('MapSheet', MapSheet);
app.component('TransactionTagSelectionSheet', TransactionTagSelectionSheet);
app.directive('TextareaAutoSize', TextareaAutoSize);
@@ -299,6 +304,10 @@ app.config.globalProperties.$locale = {
getDisplayCurrency: (value, currencyCode, notConvertValue) => getDisplayCurrency(value, currencyCode, notConvertValue, i18n.global.t),
initLocale: initLocale
};
+app.config.globalProperties.$map = {
+ leaflet: Leaflet,
+ generateOpenStreetMapTileImageUrl: services.generateOpenStreetMapTileImageUrl
+};
app.config.globalProperties.$tIf = (text, isTranslate) => transateIf(text, isTranslate, i18n.global.t);
app.config.globalProperties.$alert = (message, confirmCallback) => showAlert(message, confirmCallback, i18n.global.t);
diff --git a/src/views/mobile/transactions/EditPage.vue b/src/views/mobile/transactions/EditPage.vue
index 58e7ed02..99454ce3 100644
--- a/src/views/mobile/transactions/EditPage.vue
+++ b/src/views/mobile/transactions/EditPage.vue
@@ -38,12 +38,12 @@
-