diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e08e2677..7bd5997e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -166,7 +166,7 @@ jobs: mode: "COMMIT" configurationJson: | { - "template":"#{{CHANGELOG}}\n\nThis release note is automatically generated by GitHub Actions.\nPlease refer to [CHANGELOG.md](https://github.com/anyshake/observer/blob/master/CHANGELOG.md) for details.", + "template":"#{{CHANGELOG}}\n\nThis release note is automatically generated by GitHub Actions, please refer to [CHANGELOG.md](https://github.com/anyshake/observer/blob/master/CHANGELOG.md) for details.", "categories":[ { "title":"## Breaking Changes", diff --git a/CHANGELOG.md b/CHANGELOG.md index 789de999..b659437d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ Starting from v2.2.5, all notable changes to this project will be documented in this file. +## v3.3.0 + +### New Features + +- Added multi-user support and permission management. +- Added earthquake event source API of BMKG. +- Display AnyShake Explorer device serial number on home page. + +### Bug Fixes + +- Fix timezone issue on earthquake event source API of CEA. + +### Chore + +- Refined the module names in the log. +- Add more detailed description to API documentation. +- Replace table component with MUI in the frontend. +- Use year provided by Cloudflare to fill the timing of earthquake event source of CWA. + ## v3.2.5 ### New Features @@ -42,7 +61,7 @@ Starting from v2.2.5, all notable changes to this project will be documented in ### Bug Fixes -- Fixed time offset in EQZT data source +- Fixed time offset in EQZT data source. - Fixed Windows 7 compatibility issue. ## v3.2.2 diff --git a/VERSION b/VERSION index f08544a5..b299be97 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v3.2.5 +v3.3.0 diff --git a/api/v1/history/module.go b/api/v1/history/module.go deleted file mode 100644 index c0ba8290..00000000 --- a/api/v1/history/module.go +++ /dev/null @@ -1,115 +0,0 @@ -package history - -import ( - "errors" - "net/http" - - v1 "github.com/anyshake/observer/api/v1" - "github.com/anyshake/observer/drivers/explorer" - "github.com/anyshake/observer/server/response" - "github.com/anyshake/observer/utils/logger" - "github.com/gin-gonic/gin" -) - -// @Summary AnyShake Observer waveform history -// @Description Get waveform count data in specified time range, channel and format, the maximum duration of the waveform data to be exported is 24 hours for JSON and 1 hour for SAC -// @Router /history [post] -// @Accept application/x-www-form-urlencoded -// @Produce application/json -// @Produce application/octet-stream -// @Param start_time formData int true "Start timestamp of the waveform data to be queried, in milliseconds (unix timestamp)" -// @Param end_time formData int true "End timestamp of the waveform data to be queried, in milliseconds (unix timestamp)" -// @Param format formData string true "Format of the waveform data to be queried, `json`, `sac` or `miniseed`" -// @Param channel formData string false "Channel of the waveform, `Z`, `E` or `N`, reuqired when format is `sac` and `miniseed`" -// @Failure 400 {object} response.HttpResponse "Failed to export waveform data due to invalid format or channel" -// @Failure 410 {object} response.HttpResponse "Failed to export waveform data due to no data available" -// @Failure 500 {object} response.HttpResponse "Failed to export waveform data due to failed to read data source" -// @Success 200 {object} response.HttpResponse{data=[]explorer.ExplorerData} "Successfully exported the waveform data" -func (h *History) Register(rg *gin.RouterGroup, resolver *v1.Resolver) error { - rg.POST("/history", func(c *gin.Context) { - var binding historyBinding - if err := c.ShouldBind(&binding); err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusBadRequest) - return - } - - switch binding.Format { - case "json": - result, err := h.filterHistory(binding.StartTime, binding.EndTime, JSON_MAX_DURATION, resolver) - if err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusGone) - return - } - response.Message(c, "The waveform data was successfully filtered", result) - return - case "sac": - result, err := h.filterHistory(binding.StartTime, binding.EndTime, SAC_MAX_DURATION, resolver) - if err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusGone) - return - } - if binding.Channel != explorer.EXPLORER_CHANNEL_CODE_Z && - binding.Channel != explorer.EXPLORER_CHANNEL_CODE_E && - binding.Channel != explorer.EXPLORER_CHANNEL_CODE_N { - err := errors.New("no channel was selected") - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusBadRequest) - return - } - fileName, dataBytes, err := h.getSACBytes( - result, - resolver.Config.Stream.Station, - resolver.Config.Stream.Network, - resolver.Config.Stream.Location, - resolver.Config.Stream.Channel, - binding.Channel, - ) - if err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - - response.File(c, fileName, dataBytes) - return - case "miniseed": - result, err := h.filterHistory(binding.StartTime, binding.EndTime, SAC_MAX_DURATION, resolver) - if err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusGone) - return - } - if binding.Channel != explorer.EXPLORER_CHANNEL_CODE_Z && - binding.Channel != explorer.EXPLORER_CHANNEL_CODE_E && - binding.Channel != explorer.EXPLORER_CHANNEL_CODE_N { - err := errors.New("no channel was selected") - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusBadRequest) - return - } - fileName, dataBytes, err := h.getMiniSeedBytes( - result, - resolver.Config.Stream.Station, - resolver.Config.Stream.Network, - resolver.Config.Stream.Location, - resolver.Config.Stream.Channel, - binding.Channel, - ) - if err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - - response.File(c, fileName, dataBytes) - return - } - - response.Error(c, http.StatusBadRequest) - }) - - return nil -} diff --git a/api/v1/inventory/module.go b/api/v1/inventory/module.go deleted file mode 100644 index dd21e895..00000000 --- a/api/v1/inventory/module.go +++ /dev/null @@ -1,48 +0,0 @@ -package inventory - -import ( - "net/http" - - v1 "github.com/anyshake/observer/api/v1" - "github.com/anyshake/observer/drivers/explorer" - "github.com/anyshake/observer/server/response" - "github.com/anyshake/observer/utils/logger" - "github.com/gin-gonic/gin" -) - -// @Summary AnyShake Observer station inventory -// @Description Get SeisComP XML inventory, which contains meta data of the station -// @Router /inventory [get] -// @Param format query string false "Format of the inventory, either `json` or `xml`", default is `xml` -// @Produce application/json -// @Success 200 {object} response.HttpResponse{data=string} "Successfully get SeisComP XML inventory" -// @Produce application/xml -func (i *Inventory) Register(rg *gin.RouterGroup, resolver *v1.Resolver) error { - var explorerDeps *explorer.ExplorerDependency - err := resolver.Dependency.Invoke(func(deps *explorer.ExplorerDependency) error { - explorerDeps = deps - return nil - }) - if err != nil { - return err - } - - rg.GET("/inventory", func(c *gin.Context) { - var binding inventoryBinding - if err := c.ShouldBind(&binding); err != nil { - logger.GetLogger(i.GetApiName()).Errorln(err) - response.Error(c, http.StatusBadRequest) - return - } - - inventory := i.getInventoryString(resolver.Config, explorerDeps) - if binding.Format == "json" { - response.Message(c, "Successfully get SeisComP XML inventory", inventory) - return - } - - c.Data(http.StatusOK, "application/xml", []byte(inventory)) - }) - - return nil -} diff --git a/api/v1/mseed/module.go b/api/v1/mseed/module.go deleted file mode 100644 index 55ff1ac6..00000000 --- a/api/v1/mseed/module.go +++ /dev/null @@ -1,77 +0,0 @@ -package mseed - -import ( - "errors" - "net/http" - - v1 "github.com/anyshake/observer/api/v1" - "github.com/anyshake/observer/server/response" - "github.com/anyshake/observer/services/miniseed" - "github.com/anyshake/observer/utils/logger" - "github.com/gin-gonic/gin" -) - -// @Summary AnyShake Observer MiniSEED data -// @Description List MiniSEED data if action is `show`, or export MiniSEED data in .mseed format if action is `export` -// @Router /mseed [post] -// @Accept application/x-www-form-urlencoded -// @Produce application/json -// @Produce application/octet-stream -// @Param action formData string true "Action to be performed, either `show` or `export`" -// @Param name formData string false "Name of MiniSEED file to be exported, end with `.mseed`" -// @Failure 400 {object} response.HttpResponse "Failed to list or export MiniSEED data due to invalid request body" -// @Failure 410 {object} response.HttpResponse "Failed to export MiniSEED data due to invalid file name or permission denied" -// @Failure 500 {object} response.HttpResponse "Failed to list or export MiniSEED data due to internal server error" -// @Success 200 {object} response.HttpResponse{data=[]miniSeedFileInfo} "Successfully get list of MiniSEED files" -func (h *MSeed) Register(rg *gin.RouterGroup, resolver *v1.Resolver) error { - // Get MiniSEED service configuration - var miniseedService miniseed.MiniSeedService - serviceConfig, ok := resolver.Config.Services[miniseedService.GetServiceName()] - if !ok { - return errors.New("failed to get configuration for MiniSEED service") - } - basePath := serviceConfig.(map[string]any)["path"].(string) - lifeCycle := int(serviceConfig.(map[string]any)["lifecycle"].(float64)) - - rg.POST("/mseed", func(c *gin.Context) { - var binding mseedBinding - if err := c.ShouldBind(&binding); err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusBadRequest) - return - } - - if binding.Action == "show" { - fileList, err := h.getMiniSeedList( - basePath, - resolver.Config.Stream.Station, - resolver.Config.Stream.Network, - lifeCycle, - ) - if err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - - response.Message(c, "Successfully get MiniSEED file list", fileList) - return - } - - if len(binding.Name) == 0 { - response.Error(c, http.StatusBadRequest) - return - } - - fileBytes, err := h.getMiniSeedBytes(basePath, binding.Name) - if err != nil { - logger.GetLogger(h.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - - response.File(c, binding.Name, fileBytes) - }) - - return nil -} diff --git a/api/v1/station/module.go b/api/v1/station/module.go deleted file mode 100644 index 4bfe0741..00000000 --- a/api/v1/station/module.go +++ /dev/null @@ -1,77 +0,0 @@ -package station - -import ( - "net/http" - - v1 "github.com/anyshake/observer/api/v1" - "github.com/anyshake/observer/drivers/explorer" - "github.com/anyshake/observer/server/response" - "github.com/anyshake/observer/utils/logger" - "github.com/gin-gonic/gin" -) - -// @Summary AnyShake Observer station status -// @Description Get Observer station status including system information, memory usage, disk usage, CPU usage, ADC information, geophone information, and location information -// @Router /station [get] -// @Produce application/json -// @Success 200 {object} response.HttpResponse{data=stationInfo} "Successfully read station information" -func (s *Station) Register(rg *gin.RouterGroup, resolver *v1.Resolver) error { - var explorerDeps *explorer.ExplorerDependency - err := resolver.Dependency.Invoke(func(deps *explorer.ExplorerDependency) error { - explorerDeps = deps - return nil - }) - if err != nil { - return err - } - - rg.GET("/station", func(c *gin.Context) { - var explorer explorerInfo - err := explorer.get(resolver.TimeSource, explorerDeps) - if err != nil { - logger.GetLogger(s.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - var cpu cpuInfo - err = cpu.get() - if err != nil { - logger.GetLogger(s.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - var disk diskInfo - err = disk.get() - if err != nil { - logger.GetLogger(s.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - var memory memoryInfo - err = memory.get() - if err != nil { - logger.GetLogger(s.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - var os osInfo - err = os.get(resolver.TimeSource) - if err != nil { - logger.GetLogger(s.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - response.Message(c, "Successfully read station information", stationInfo{ - Station: resolver.Config.Station, - Stream: resolver.Config.Stream, - Sensor: resolver.Config.Sensor, - Explorer: explorer, - CPU: cpu, - Disk: disk, - Memory: memory, - OS: os, - }) - }) - - return nil -} diff --git a/api/v1/trace/module.go b/api/v1/trace/module.go deleted file mode 100644 index e24b2e74..00000000 --- a/api/v1/trace/module.go +++ /dev/null @@ -1,79 +0,0 @@ -package trace - -import ( - "net/http" - "time" - - v1 "github.com/anyshake/observer/api/v1" - "github.com/anyshake/observer/drivers/explorer" - "github.com/anyshake/observer/server/response" - "github.com/anyshake/observer/utils/logger" - "github.com/anyshake/observer/utils/seisevent" - "github.com/gin-gonic/gin" -) - -// @Summary AnyShake Observer event trace -// @Description Get list of earthquake events data source and earthquake events from specified data source -// @Router /trace [post] -// @Accept application/x-www-form-urlencoded -// @Produce application/json -// @Param source formData string true "Use `show` to get available sources first, then choose one and request again to get events" -// @Failure 400 {object} response.HttpResponse "Failed to read earthquake event list due to invalid data source" -// @Failure 500 {object} response.HttpResponse "Failed to read earthquake event list due to failed to read data source" -// @Success 200 {object} response.HttpResponse{data=[]seisevent.Event} "Successfully read the list of earthquake events" -func (t *Trace) Register(rg *gin.RouterGroup, resolver *v1.Resolver) error { - var explorerDeps *explorer.ExplorerDependency - err := resolver.Dependency.Invoke(func(deps *explorer.ExplorerDependency) error { - explorerDeps = deps - return nil - }) - if err != nil { - return err - } - - seisSources := seisevent.New(30 * time.Second) - - rg.POST("/trace", func(c *gin.Context) { - var binding traceBinding - if err := c.ShouldBind(&binding); err != nil { - logger.GetLogger(t.GetApiName()).Errorln(err) - response.Error(c, http.StatusBadRequest) - return - } - - if binding.Source == "show" { - var properties []seisevent.DataSourceProperty - for _, source := range seisSources { - properties = append(properties, source.GetProperty()) - } - - response.Message(c, "Successfully read available data source properties", properties) - return - } - - if err != nil { - logger.GetLogger(t.GetApiName()).Errorln(err) - return - } - var ( - source, ok = seisSources[binding.Source] - latitude = explorerDeps.Config.GetLatitude() - longitude = explorerDeps.Config.GetLongitude() - ) - if ok { - events, err := source.GetEvents(latitude, longitude) - if err != nil { - logger.GetLogger(t.GetApiName()).Errorln(err) - response.Error(c, http.StatusInternalServerError) - return - } - - response.Message(c, "Successfully read the list of earthquake events", events) - return - } - - response.Error(c, http.StatusBadRequest) - }) - - return nil -} diff --git a/build/assets/config.json b/build/assets/config.json index bb2d521b..9fd5a8ba 100644 --- a/build/assets/config.json +++ b/build/assets/config.json @@ -14,7 +14,7 @@ "explorer_settings": { "dsn": "transport:///dev/ttyUSB0?baudrate=115200", "engine": "serial", - "legacy": true + "legacy": false }, "sensor_settings": { "frequency": 4.5, @@ -49,7 +49,8 @@ "port": 8073, "cors": true, "debug": true, - "rate": 30 + "rate": 30, + "restrict": false }, "logger_settings": { "level": "info", diff --git a/cmd/main.go b/cmd/main.go index e8ddc656..c871902c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,6 +28,7 @@ import ( service_watchdog "github.com/anyshake/observer/services/watchdog" "github.com/anyshake/observer/startups" startup_explorer "github.com/anyshake/observer/startups/explorer" + startup_setup_admin "github.com/anyshake/observer/startups/setup_admin" "github.com/anyshake/observer/utils/logger" "github.com/anyshake/observer/utils/timesource" "github.com/common-nighthawk/go-figure" @@ -74,8 +75,8 @@ func init() { } // @BasePath /api/v1 -// @title AnyShake Observer APIv1 -// @description This is APIv1 documentation for AnyShake Observer, please set `server_settings.debug` to `false` in `config.json` when deploying to production environment in case of any security issues. +// @title AnyShake Observer API v1 +// @description This is API v1 documentation for AnyShake Observer, please set `server_settings.debug` to `false` in `config.json` when deploying to production environment in case of any security issues. func main() { args := parseCommandLine() printVersion() @@ -161,6 +162,7 @@ func main() { // Setup startup tasks and provide dependencies startupTasks := []startups.StartupTask{ &startup_explorer.ExplorerStartupTask{CancelToken: cancelToken}, + &startup_setup_admin.SetupAdminStartupTask{CancelToken: cancelToken}, } startupOptions := &startups.Options{ Config: &conf, @@ -206,19 +208,12 @@ func main() { } // Start HTTP server - go server.Serve( - conf.Server.Host, - conf.Server.Port, - &server.Options{ - CORS: conf.Server.CORS, - DebugMode: conf.Server.Debug, - GzipLevel: GZIP_LEVEL, - RateFactor: conf.Server.Rate, - WebPrefix: WEB_PREFIX, - ApiPrefix: API_PREFIX, - ServicesOptions: serviceOptions, - }) - logger.GetLogger(main).Infof("web server is listening on %s:%d", conf.Server.Host, conf.Server.Port) + srv := &server.ServerImpl{ + GzipLevel: GZIP_LEVEL, + WebPrefix: WEB_PREFIX, + ApiPrefix: API_PREFIX, + } + go srv.Start(logger.GetLogger("server"), serviceOptions) // Receive interrupt signals osSignal := make(chan os.Signal, 1) diff --git a/cmd/migrate.go b/cmd/migrate.go index 7c8afbee..1df6f712 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -7,5 +7,16 @@ import ( ) func migrate(databaseConn *gorm.DB) error { - return dao.Migrate(databaseConn, &tables.AdcCount{}) + appTables := []dao.Table{ + &tables.AdcCount{}, + &tables.SysUser{}, + } + for _, table := range appTables { + err := dao.Migrate(databaseConn, table) + if err != nil { + return err + } + } + + return nil } diff --git a/config/types.go b/config/types.go index bee99fa0..ce9a2f56 100644 --- a/config/types.go +++ b/config/types.go @@ -54,11 +54,12 @@ type database struct { } type server struct { - Host string `json:"host"` - Port int `json:"port" validate:"min=1,max=65535"` - CORS bool `json:"cors"` - Debug bool `json:"debug"` - Rate int `json:"rate" validate:"gte=0"` + Host string `json:"host"` + Port int `json:"port" validate:"min=1,max=65535"` + CORS bool `json:"cors"` + Debug bool `json:"debug"` + Restrict bool `json:"restrict"` + Rate int `json:"rate" validate:"gte=0"` } type logger struct { diff --git a/docs/docs.go b/docs/docs.go index 8f828a14..fe23e6e1 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,584 +15,278 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/auth": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "In restricted mode, the client must log in to access other APIs. This API is used to checks the server's authentication status, issues an RSA public key for credential encryption, generates a captcha, authenticates the client, and signs or refreshes the JWT token. This API requires a valid JWT token if action is ` + "`" + `refresh` + "`" + `.", + "produces": [ + "application/json" + ], + "summary": "User Authentication", + "parameters": [ + { + "type": "string", + "description": "Specifies the action to be performed. Use ` + "`" + `inspect` + "`" + ` to check the server's restriction status, ` + "`" + `preauth` + "`" + ` to get a Base64 RSA public key in PEM format and generate a Base64 captcha PNG image, ` + "`" + `login` + "`" + ` to authenticate the client using encrypted credentials, and ` + "`" + `refresh` + "`" + ` to refresh the JWT token.", + "name": "action", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "A unique string used to prevent replay attacks, required for the ` + "`" + `login` + "`" + ` action and left empty for other actions. The nonce is the SHA-1 hash of the RSA public key from the pre-authentication stage and becomes invalid once the request is sent. It also expires if unused within the time-to-live (TTL) period, which is set during the pre-authentication stage.", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "Base64 encrypted credential using the RSA public key, required for the ` + "`" + `login` + "`" + ` action and left empty for other actions. The decrypted credential is a JSON object that includes the username, password, captcha ID, and captcha solution. Example: ` + "`" + `{ username: admin, password: admin, captcha_id: 123, captcha_solution: abc }` + "`" + `.", + "name": "credential", + "in": "formData" + }, + { + "type": "string", + "description": "Bearer JWT token, only required for the ` + "`" + `refresh` + "`" + ` action.", + "name": "Authorization", + "in": "header" + } + ], + "responses": {} + } + }, "/history": { "post": { - "description": "Get waveform count data in specified time range, channel and format, the maximum duration of the waveform data to be exported is 24 hours for JSON and 1 hour for SAC", - "consumes": [ - "application/x-www-form-urlencoded" + "security": [ + { + "ApiKeyAuth": [] + } ], + "description": "Get seismic waveform data from database in specified time range, channel and format. This API supports 1 hour of maximum duration of the waveform data to be queried. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json", "application/octet-stream" ], - "summary": "AnyShake Observer waveform history", + "summary": "Waveform History", "parameters": [ { "type": "integer", - "description": "Start timestamp of the waveform data to be queried, in milliseconds (unix timestamp)", + "description": "Start time of the waveform to be queried, unix timestamp format in milliseconds.", "name": "start_time", "in": "formData", "required": true }, { "type": "integer", - "description": "End timestamp of the waveform data to be queried, in milliseconds (unix timestamp)", + "description": "End time of the waveform to be queried, unix timestamp format in milliseconds.", "name": "end_time", "in": "formData", "required": true }, { "type": "string", - "description": "Format of the waveform data to be queried, ` + "`" + `json` + "`" + `, ` + "`" + `sac` + "`" + ` or ` + "`" + `miniseed` + "`" + `", + "description": "Set output format of the waveform data, available options are ` + "`" + `json` + "`" + `, ` + "`" + `sac` + "`" + `, and ` + "`" + `miniseed` + "`" + `.", "name": "format", "in": "formData", "required": true }, { "type": "string", - "description": "Channel of the waveform, ` + "`" + `Z` + "`" + `, ` + "`" + `E` + "`" + ` or ` + "`" + `N` + "`" + `, reuqired when format is ` + "`" + `sac` + "`" + ` and ` + "`" + `miniseed` + "`" + `", + "description": "Channel of the waveform, available options are ` + "`" + `Z` + "`" + `, ` + "`" + `E` + "`" + ` or ` + "`" + `N` + "`" + ` (in uppercase), only reuqired when output format is set to ` + "`" + `sac` + "`" + ` and ` + "`" + `miniseed` + "`" + `.", "name": "channel", "in": "formData" - } - ], - "responses": { - "200": { - "description": "Successfully exported the waveform data", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/explorer.ExplorerData" - } - } - } - } - ] - } - }, - "400": { - "description": "Failed to export waveform data due to invalid format or channel", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } }, - "410": { - "description": "Failed to export waveform data due to no data available", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } - }, - "500": { - "description": "Failed to export waveform data due to failed to read data source", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } - } + ], + "responses": {} } }, "/inventory": { "get": { - "description": "Get SeisComP XML inventory, which contains meta data of the station", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get SeisComP XML inventory, which contains meta data of the station. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json", - "application/xml" + " application/xml" ], - "summary": "AnyShake Observer station inventory", + "summary": "Station Inventory", "parameters": [ { "type": "string", - "description": "Format of the inventory, either ` + "`" + `json` + "`" + ` or ` + "`" + `xml` + "`" + `", + "description": "Format of the inventory, available options are ` + "`" + `json` + "`" + ` or ` + "`" + `xml` + "`" + `", "name": "format", "in": "query" + }, + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } ], - "responses": { - "200": { - "description": "Successfully get SeisComP XML inventory", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "string" - } - } - } - ] - } - } - } + "responses": {} } }, "/mseed": { "post": { - "description": "List MiniSEED data if action is ` + "`" + `show` + "`" + `, or export MiniSEED data in .mseed format if action is ` + "`" + `export` + "`" + `", - "consumes": [ - "application/x-www-form-urlencoded" + "security": [ + { + "ApiKeyAuth": [] + } ], + "description": "This API returns a list of MiniSEED files or exports a specific MiniSEED file. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json", "application/octet-stream" ], - "summary": "AnyShake Observer MiniSEED data", + "summary": "MiniSEED Data", "parameters": [ { "type": "string", - "description": "Action to be performed, either ` + "`" + `show` + "`" + ` or ` + "`" + `export` + "`" + `", + "description": "Action to be performed, Use ` + "`" + `list` + "`" + ` to get list of MiniSEED files, ` + "`" + `export` + "`" + ` to export a specific MiniSEED file.", "name": "action", "in": "formData", "required": true }, { "type": "string", - "description": "Name of MiniSEED file to be exported, end with ` + "`" + `.mseed` + "`" + `", + "description": "A valid filename of the MiniSEED file to be exported, only required when action is ` + "`" + `export` + "`" + `.", "name": "name", "in": "formData" - } - ], - "responses": { - "200": { - "description": "Successfully get list of MiniSEED files", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/mseed.miniSeedFileInfo" - } - } - } - } - ] - } - }, - "400": { - "description": "Failed to list or export MiniSEED data due to invalid request body", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } - }, - "410": { - "description": "Failed to export MiniSEED data due to invalid file name or permission denied", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } }, - "500": { - "description": "Failed to list or export MiniSEED data due to internal server error", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } - } + ], + "responses": {} } }, "/station": { "get": { - "description": "Get Observer station status including system information, memory usage, disk usage, CPU usage, ADC information, geophone information, and location information", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get Observer station status including system information, memory usage, disk usage, CPU usage, ADC information, geophone information, and location information. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json" ], - "summary": "AnyShake Observer station status", - "responses": { - "200": { - "description": "Successfully read station information", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/station.stationInfo" - } - } - } - ] - } + "summary": "Station Status", + "parameters": [ + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } - } + ], + "responses": {} } }, "/trace": { "post": { - "description": "Get list of earthquake events data source and earthquake events from specified data source", - "consumes": [ - "application/x-www-form-urlencoded" + "security": [ + { + "ApiKeyAuth": [] + } ], + "description": "This API retrieves seismic events from the specified data source, including essential information such as event time, location, magnitude, depth and estimated distance and arrival time from the station. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json" ], - "summary": "AnyShake Observer event trace", + "summary": "Seismic Trace", "parameters": [ { "type": "string", - "description": "Use ` + "`" + `show` + "`" + ` to get available sources first, then choose one and request again to get events", + "description": "Use ` + "`" + `list` + "`" + ` to get available sources first, then choose one and request again to get events", "name": "source", "in": "formData", "required": true - } - ], - "responses": { - "200": { - "description": "Successfully read the list of earthquake events", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/seisevent.Event" - } - } - } - } - ] - } }, - "400": { - "description": "Failed to read earthquake event list due to invalid data source", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } - }, - "500": { - "description": "Failed to read earthquake event list due to failed to read data source", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } - } - } - } - }, - "definitions": { - "config.Sensor": { - "type": "object", - "properties": { - "frequency": { - "type": "number" - }, - "fullscale": { - "type": "number" - }, - "resolution": { - "type": "integer" - }, - "sensitivity": { - "type": "number" - }, - "velocity": { - "type": "boolean" - }, - "vref": { - "type": "number" - } - } - }, - "config.Station": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "region": { - "type": "string" - } - } - }, - "config.Stream": { - "type": "object", - "properties": { - "channel": { - "type": "string", - "maxLength": 3 - }, - "location": { - "type": "string", - "maxLength": 2 - }, - "network": { - "type": "string", - "maxLength": 2 - }, - "station": { - "type": "string", - "maxLength": 5 - } + ], + "responses": {} } }, - "explorer.ExplorerData": { - "type": "object", - "properties": { - "e_axis": { - "type": "array", - "items": { - "type": "integer" - } - }, - "n_axis": { - "type": "array", - "items": { - "type": "integer" + "/user": { + "post": { + "security": [ + { + "ApiKeyAuth": [] } - }, - "sample_rate": { - "type": "integer" - }, - "timestamp": { - "type": "integer" - }, - "z_axis": { - "type": "array", - "items": { - "type": "integer" + ], + "description": "This API is used to manage user accounts, including creating, removing, and editing user profiles. This API only available in restricted mode and requires a valid JWT token.", + "produces": [ + "application/json" + ], + "summary": "User Management", + "parameters": [ + { + "type": "string", + "description": "Specifies the action to be performed. Use ` + "`" + `preauth` + "`" + ` to get a Base64 RSA public key in PEM format, ` + "`" + `profile` + "`" + ` to get profile of current user, ` + "`" + `list` + "`" + ` to get list of all users (admin only), ` + "`" + `create` + "`" + ` to create a new user (admin only), ` + "`" + `remove` + "`" + ` to remove a user (admin only), and ` + "`" + `edit` + "`" + ` to edit a user (admin only).", + "name": "action", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "A unique string used to prevent replay attacks, required for the ` + "`" + `create` + "`" + `, ` + "`" + `remove` + "`" + `, ` + "`" + `edit` + "`" + ` actions and left empty for other actions. The nonce is the SHA-1 hash of the RSA public key from the pre-authentication stage and becomes invalid once the request is sent. It also expires if unused within the time-to-live (TTL) period, which is set during the pre-authentication stage.", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "The user ID to be removed or edited, required for the ` + "`" + `remove` + "`" + ` and ` + "`" + `edit` + "`" + ` actions and left empty for other actions. The user ID is encrypted with the RSA public key.", + "name": "user_id", + "in": "formData" + }, + { + "type": "boolean", + "description": "Specifies whether the user is an administrator, required for the ` + "`" + `create` + "`" + ` and ` + "`" + `edit` + "`" + ` actions and set to false in other actions.", + "name": "admin", + "in": "formData" + }, + { + "type": "string", + "description": "The username of the user to be created or edited, required for the ` + "`" + `create` + "`" + ` and ` + "`" + `edit` + "`" + ` actions and left empty for other actions. The username is encrypted with the RSA public key.", + "name": "username", + "in": "formData" + }, + { + "type": "string", + "description": "The password of the user to be created or edited, required for the ` + "`" + `create` + "`" + ` and ` + "`" + `edit` + "`" + ` actions and left empty for other actions. The password is encrypted with the RSA public key.", + "name": "password", + "in": "formData" + }, + { + "type": "string", + "description": "Bearer JWT token.", + "name": "Authorization", + "in": "header", + "required": true } - } - } - }, - "mseed.miniSeedFileInfo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "size": { - "type": "string" - }, - "time": { - "type": "integer" - }, - "ttl": { - "type": "integer" - } - } - }, - "response.HttpResponse": { - "type": "object", - "properties": { - "data": {}, - "error": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "path": { - "type": "string" - }, - "status": { - "type": "integer" - }, - "time": { - "type": "string" - } - } - }, - "seisevent.Estimation": { - "type": "object", - "properties": { - "p": { - "type": "number" - }, - "s": { - "type": "number" - } - } - }, - "seisevent.Event": { - "type": "object", - "properties": { - "depth": { - "type": "number" - }, - "distance": { - "type": "number" - }, - "estimation": { - "$ref": "#/definitions/seisevent.Estimation" - }, - "event": { - "type": "string" - }, - "latitude": { - "type": "number" - }, - "longitude": { - "type": "number" - }, - "magnitude": { - "type": "number" - }, - "region": { - "type": "string" - }, - "timestamp": { - "type": "integer" - }, - "verfied": { - "type": "boolean" - } - } - }, - "station.cpuInfo": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "percent": { - "type": "number" - } - } - }, - "station.diskInfo": { - "type": "object", - "properties": { - "free": { - "type": "integer" - }, - "percent": { - "type": "number" - }, - "total": { - "type": "integer" - }, - "used": { - "type": "integer" - } - } - }, - "station.explorerInfo": { - "type": "object", - "properties": { - "device_id": { - "type": "integer" - }, - "elapsed": { - "type": "integer" - }, - "elevation": { - "type": "number" - }, - "errors": { - "type": "integer" - }, - "latitude": { - "type": "number" - }, - "longitude": { - "type": "number" - }, - "received": { - "type": "integer" - }, - "sample_rate": { - "type": "integer" - } - } - }, - "station.memoryInfo": { - "type": "object", - "properties": { - "free": { - "type": "integer" - }, - "percent": { - "type": "number" - }, - "total": { - "type": "integer" - }, - "used": { - "type": "integer" - } - } - }, - "station.osInfo": { - "type": "object", - "properties": { - "arch": { - "type": "string" - }, - "distro": { - "type": "string" - }, - "hostname": { - "type": "string" - }, - "os": { - "type": "string" - }, - "timestamp": { - "type": "integer" - }, - "uptime": { - "type": "integer" - } - } - }, - "station.stationInfo": { - "type": "object", - "properties": { - "cpu": { - "$ref": "#/definitions/station.cpuInfo" - }, - "disk": { - "$ref": "#/definitions/station.diskInfo" - }, - "explorer": { - "$ref": "#/definitions/station.explorerInfo" - }, - "memory": { - "$ref": "#/definitions/station.memoryInfo" - }, - "os": { - "$ref": "#/definitions/station.osInfo" - }, - "sensor": { - "$ref": "#/definitions/config.Sensor" - }, - "station": { - "$ref": "#/definitions/config.Station" - }, - "stream": { - "$ref": "#/definitions/config.Stream" - } + ], + "responses": {} } } } @@ -604,8 +298,8 @@ var SwaggerInfo = &swag.Spec{ Host: "", BasePath: "/api/v1", Schemes: []string{}, - Title: "AnyShake Observer APIv1", - Description: "This is APIv1 documentation for AnyShake Observer, please set `server_settings.debug` to `false` in `config.json` when deploying to production environment in case of any security issues.", + Title: "AnyShake Observer API v1", + Description: "This is API v1 documentation for AnyShake Observer, please set `server_settings.debug` to `false` in `config.json` when deploying to production environment in case of any security issues.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index 5c044651..bc0753fb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,590 +1,284 @@ { "swagger": "2.0", "info": { - "description": "This is APIv1 documentation for AnyShake Observer, please set `server_settings.debug` to `false` in `config.json` when deploying to production environment in case of any security issues.", - "title": "AnyShake Observer APIv1", + "description": "This is API v1 documentation for AnyShake Observer, please set `server_settings.debug` to `false` in `config.json` when deploying to production environment in case of any security issues.", + "title": "AnyShake Observer API v1", "contact": {} }, "basePath": "/api/v1", "paths": { + "/auth": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "In restricted mode, the client must log in to access other APIs. This API is used to checks the server's authentication status, issues an RSA public key for credential encryption, generates a captcha, authenticates the client, and signs or refreshes the JWT token. This API requires a valid JWT token if action is `refresh`.", + "produces": [ + "application/json" + ], + "summary": "User Authentication", + "parameters": [ + { + "type": "string", + "description": "Specifies the action to be performed. Use `inspect` to check the server's restriction status, `preauth` to get a Base64 RSA public key in PEM format and generate a Base64 captcha PNG image, `login` to authenticate the client using encrypted credentials, and `refresh` to refresh the JWT token.", + "name": "action", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "A unique string used to prevent replay attacks, required for the `login` action and left empty for other actions. The nonce is the SHA-1 hash of the RSA public key from the pre-authentication stage and becomes invalid once the request is sent. It also expires if unused within the time-to-live (TTL) period, which is set during the pre-authentication stage.", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "Base64 encrypted credential using the RSA public key, required for the `login` action and left empty for other actions. The decrypted credential is a JSON object that includes the username, password, captcha ID, and captcha solution. Example: `{ username: admin, password: admin, captcha_id: 123, captcha_solution: abc }`.", + "name": "credential", + "in": "formData" + }, + { + "type": "string", + "description": "Bearer JWT token, only required for the `refresh` action.", + "name": "Authorization", + "in": "header" + } + ], + "responses": {} + } + }, "/history": { "post": { - "description": "Get waveform count data in specified time range, channel and format, the maximum duration of the waveform data to be exported is 24 hours for JSON and 1 hour for SAC", - "consumes": [ - "application/x-www-form-urlencoded" + "security": [ + { + "ApiKeyAuth": [] + } ], + "description": "Get seismic waveform data from database in specified time range, channel and format. This API supports 1 hour of maximum duration of the waveform data to be queried. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json", "application/octet-stream" ], - "summary": "AnyShake Observer waveform history", + "summary": "Waveform History", "parameters": [ { "type": "integer", - "description": "Start timestamp of the waveform data to be queried, in milliseconds (unix timestamp)", + "description": "Start time of the waveform to be queried, unix timestamp format in milliseconds.", "name": "start_time", "in": "formData", "required": true }, { "type": "integer", - "description": "End timestamp of the waveform data to be queried, in milliseconds (unix timestamp)", + "description": "End time of the waveform to be queried, unix timestamp format in milliseconds.", "name": "end_time", "in": "formData", "required": true }, { "type": "string", - "description": "Format of the waveform data to be queried, `json`, `sac` or `miniseed`", + "description": "Set output format of the waveform data, available options are `json`, `sac`, and `miniseed`.", "name": "format", "in": "formData", "required": true }, { "type": "string", - "description": "Channel of the waveform, `Z`, `E` or `N`, reuqired when format is `sac` and `miniseed`", + "description": "Channel of the waveform, available options are `Z`, `E` or `N` (in uppercase), only reuqired when output format is set to `sac` and `miniseed`.", "name": "channel", "in": "formData" - } - ], - "responses": { - "200": { - "description": "Successfully exported the waveform data", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/explorer.ExplorerData" - } - } - } - } - ] - } - }, - "400": { - "description": "Failed to export waveform data due to invalid format or channel", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } }, - "410": { - "description": "Failed to export waveform data due to no data available", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } - }, - "500": { - "description": "Failed to export waveform data due to failed to read data source", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } - } + ], + "responses": {} } }, "/inventory": { "get": { - "description": "Get SeisComP XML inventory, which contains meta data of the station", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get SeisComP XML inventory, which contains meta data of the station. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json", - "application/xml" + " application/xml" ], - "summary": "AnyShake Observer station inventory", + "summary": "Station Inventory", "parameters": [ { "type": "string", - "description": "Format of the inventory, either `json` or `xml`", + "description": "Format of the inventory, available options are `json` or `xml`", "name": "format", "in": "query" + }, + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } ], - "responses": { - "200": { - "description": "Successfully get SeisComP XML inventory", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "string" - } - } - } - ] - } - } - } + "responses": {} } }, "/mseed": { "post": { - "description": "List MiniSEED data if action is `show`, or export MiniSEED data in .mseed format if action is `export`", - "consumes": [ - "application/x-www-form-urlencoded" + "security": [ + { + "ApiKeyAuth": [] + } ], + "description": "This API returns a list of MiniSEED files or exports a specific MiniSEED file. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json", "application/octet-stream" ], - "summary": "AnyShake Observer MiniSEED data", + "summary": "MiniSEED Data", "parameters": [ { "type": "string", - "description": "Action to be performed, either `show` or `export`", + "description": "Action to be performed, Use `list` to get list of MiniSEED files, `export` to export a specific MiniSEED file.", "name": "action", "in": "formData", "required": true }, { "type": "string", - "description": "Name of MiniSEED file to be exported, end with `.mseed`", + "description": "A valid filename of the MiniSEED file to be exported, only required when action is `export`.", "name": "name", "in": "formData" - } - ], - "responses": { - "200": { - "description": "Successfully get list of MiniSEED files", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/mseed.miniSeedFileInfo" - } - } - } - } - ] - } - }, - "400": { - "description": "Failed to list or export MiniSEED data due to invalid request body", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } - }, - "410": { - "description": "Failed to export MiniSEED data due to invalid file name or permission denied", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } }, - "500": { - "description": "Failed to list or export MiniSEED data due to internal server error", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } - } + ], + "responses": {} } }, "/station": { "get": { - "description": "Get Observer station status including system information, memory usage, disk usage, CPU usage, ADC information, geophone information, and location information", + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Get Observer station status including system information, memory usage, disk usage, CPU usage, ADC information, geophone information, and location information. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json" ], - "summary": "AnyShake Observer station status", - "responses": { - "200": { - "description": "Successfully read station information", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/station.stationInfo" - } - } - } - ] - } + "summary": "Station Status", + "parameters": [ + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } - } + ], + "responses": {} } }, "/trace": { "post": { - "description": "Get list of earthquake events data source and earthquake events from specified data source", - "consumes": [ - "application/x-www-form-urlencoded" + "security": [ + { + "ApiKeyAuth": [] + } ], + "description": "This API retrieves seismic events from the specified data source, including essential information such as event time, location, magnitude, depth and estimated distance and arrival time from the station. This API requires a valid JWT token if the server is in restricted mode.", "produces": [ "application/json" ], - "summary": "AnyShake Observer event trace", + "summary": "Seismic Trace", "parameters": [ { "type": "string", - "description": "Use `show` to get available sources first, then choose one and request again to get events", + "description": "Use `list` to get available sources first, then choose one and request again to get events", "name": "source", "in": "formData", "required": true - } - ], - "responses": { - "200": { - "description": "Successfully read the list of earthquake events", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.HttpResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/seisevent.Event" - } - } - } - } - ] - } }, - "400": { - "description": "Failed to read earthquake event list due to invalid data source", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } - }, - "500": { - "description": "Failed to read earthquake event list due to failed to read data source", - "schema": { - "$ref": "#/definitions/response.HttpResponse" - } + { + "type": "string", + "description": "Bearer JWT token, only required when the server is in restricted mode.", + "name": "Authorization", + "in": "header" } - } - } - } - }, - "definitions": { - "config.Sensor": { - "type": "object", - "properties": { - "frequency": { - "type": "number" - }, - "fullscale": { - "type": "number" - }, - "resolution": { - "type": "integer" - }, - "sensitivity": { - "type": "number" - }, - "velocity": { - "type": "boolean" - }, - "vref": { - "type": "number" - } - } - }, - "config.Station": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "region": { - "type": "string" - } - } - }, - "config.Stream": { - "type": "object", - "properties": { - "channel": { - "type": "string", - "maxLength": 3 - }, - "location": { - "type": "string", - "maxLength": 2 - }, - "network": { - "type": "string", - "maxLength": 2 - }, - "station": { - "type": "string", - "maxLength": 5 - } + ], + "responses": {} } }, - "explorer.ExplorerData": { - "type": "object", - "properties": { - "e_axis": { - "type": "array", - "items": { - "type": "integer" - } - }, - "n_axis": { - "type": "array", - "items": { - "type": "integer" + "/user": { + "post": { + "security": [ + { + "ApiKeyAuth": [] } - }, - "sample_rate": { - "type": "integer" - }, - "timestamp": { - "type": "integer" - }, - "z_axis": { - "type": "array", - "items": { - "type": "integer" + ], + "description": "This API is used to manage user accounts, including creating, removing, and editing user profiles. This API only available in restricted mode and requires a valid JWT token.", + "produces": [ + "application/json" + ], + "summary": "User Management", + "parameters": [ + { + "type": "string", + "description": "Specifies the action to be performed. Use `preauth` to get a Base64 RSA public key in PEM format, `profile` to get profile of current user, `list` to get list of all users (admin only), `create` to create a new user (admin only), `remove` to remove a user (admin only), and `edit` to edit a user (admin only).", + "name": "action", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "A unique string used to prevent replay attacks, required for the `create`, `remove`, `edit` actions and left empty for other actions. The nonce is the SHA-1 hash of the RSA public key from the pre-authentication stage and becomes invalid once the request is sent. It also expires if unused within the time-to-live (TTL) period, which is set during the pre-authentication stage.", + "name": "nonce", + "in": "formData" + }, + { + "type": "string", + "description": "The user ID to be removed or edited, required for the `remove` and `edit` actions and left empty for other actions. The user ID is encrypted with the RSA public key.", + "name": "user_id", + "in": "formData" + }, + { + "type": "boolean", + "description": "Specifies whether the user is an administrator, required for the `create` and `edit` actions and set to false in other actions.", + "name": "admin", + "in": "formData" + }, + { + "type": "string", + "description": "The username of the user to be created or edited, required for the `create` and `edit` actions and left empty for other actions. The username is encrypted with the RSA public key.", + "name": "username", + "in": "formData" + }, + { + "type": "string", + "description": "The password of the user to be created or edited, required for the `create` and `edit` actions and left empty for other actions. The password is encrypted with the RSA public key.", + "name": "password", + "in": "formData" + }, + { + "type": "string", + "description": "Bearer JWT token.", + "name": "Authorization", + "in": "header", + "required": true } - } - } - }, - "mseed.miniSeedFileInfo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "size": { - "type": "string" - }, - "time": { - "type": "integer" - }, - "ttl": { - "type": "integer" - } - } - }, - "response.HttpResponse": { - "type": "object", - "properties": { - "data": {}, - "error": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "path": { - "type": "string" - }, - "status": { - "type": "integer" - }, - "time": { - "type": "string" - } - } - }, - "seisevent.Estimation": { - "type": "object", - "properties": { - "p": { - "type": "number" - }, - "s": { - "type": "number" - } - } - }, - "seisevent.Event": { - "type": "object", - "properties": { - "depth": { - "type": "number" - }, - "distance": { - "type": "number" - }, - "estimation": { - "$ref": "#/definitions/seisevent.Estimation" - }, - "event": { - "type": "string" - }, - "latitude": { - "type": "number" - }, - "longitude": { - "type": "number" - }, - "magnitude": { - "type": "number" - }, - "region": { - "type": "string" - }, - "timestamp": { - "type": "integer" - }, - "verfied": { - "type": "boolean" - } - } - }, - "station.cpuInfo": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "percent": { - "type": "number" - } - } - }, - "station.diskInfo": { - "type": "object", - "properties": { - "free": { - "type": "integer" - }, - "percent": { - "type": "number" - }, - "total": { - "type": "integer" - }, - "used": { - "type": "integer" - } - } - }, - "station.explorerInfo": { - "type": "object", - "properties": { - "device_id": { - "type": "integer" - }, - "elapsed": { - "type": "integer" - }, - "elevation": { - "type": "number" - }, - "errors": { - "type": "integer" - }, - "latitude": { - "type": "number" - }, - "longitude": { - "type": "number" - }, - "received": { - "type": "integer" - }, - "sample_rate": { - "type": "integer" - } - } - }, - "station.memoryInfo": { - "type": "object", - "properties": { - "free": { - "type": "integer" - }, - "percent": { - "type": "number" - }, - "total": { - "type": "integer" - }, - "used": { - "type": "integer" - } - } - }, - "station.osInfo": { - "type": "object", - "properties": { - "arch": { - "type": "string" - }, - "distro": { - "type": "string" - }, - "hostname": { - "type": "string" - }, - "os": { - "type": "string" - }, - "timestamp": { - "type": "integer" - }, - "uptime": { - "type": "integer" - } - } - }, - "station.stationInfo": { - "type": "object", - "properties": { - "cpu": { - "$ref": "#/definitions/station.cpuInfo" - }, - "disk": { - "$ref": "#/definitions/station.diskInfo" - }, - "explorer": { - "$ref": "#/definitions/station.explorerInfo" - }, - "memory": { - "$ref": "#/definitions/station.memoryInfo" - }, - "os": { - "$ref": "#/definitions/station.osInfo" - }, - "sensor": { - "$ref": "#/definitions/config.Sensor" - }, - "station": { - "$ref": "#/definitions/config.Station" - }, - "stream": { - "$ref": "#/definitions/config.Stream" - } + ], + "responses": {} } } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 085c6050..e2fcd285 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,391 +1,244 @@ basePath: /api/v1 -definitions: - config.Sensor: - properties: - frequency: - type: number - fullscale: - type: number - resolution: - type: integer - sensitivity: - type: number - velocity: - type: boolean - vref: - type: number - type: object - config.Station: - properties: - city: - type: string - country: - type: string - name: - type: string - owner: - type: string - region: - type: string - type: object - config.Stream: - properties: - channel: - maxLength: 3 - type: string - location: - maxLength: 2 - type: string - network: - maxLength: 2 - type: string - station: - maxLength: 5 - type: string - type: object - explorer.ExplorerData: - properties: - e_axis: - items: - type: integer - type: array - n_axis: - items: - type: integer - type: array - sample_rate: - type: integer - timestamp: - type: integer - z_axis: - items: - type: integer - type: array - type: object - mseed.miniSeedFileInfo: - properties: - name: - type: string - size: - type: string - time: - type: integer - ttl: - type: integer - type: object - response.HttpResponse: - properties: - data: {} - error: - type: boolean - message: - type: string - path: - type: string - status: - type: integer - time: - type: string - type: object - seisevent.Estimation: - properties: - p: - type: number - s: - type: number - type: object - seisevent.Event: - properties: - depth: - type: number - distance: - type: number - estimation: - $ref: '#/definitions/seisevent.Estimation' - event: - type: string - latitude: - type: number - longitude: - type: number - magnitude: - type: number - region: - type: string - timestamp: - type: integer - verfied: - type: boolean - type: object - station.cpuInfo: - properties: - model: - type: string - percent: - type: number - type: object - station.diskInfo: - properties: - free: - type: integer - percent: - type: number - total: - type: integer - used: - type: integer - type: object - station.explorerInfo: - properties: - device_id: - type: integer - elapsed: - type: integer - elevation: - type: number - errors: - type: integer - latitude: - type: number - longitude: - type: number - received: - type: integer - sample_rate: - type: integer - type: object - station.memoryInfo: - properties: - free: - type: integer - percent: - type: number - total: - type: integer - used: - type: integer - type: object - station.osInfo: - properties: - arch: - type: string - distro: - type: string - hostname: - type: string - os: - type: string - timestamp: - type: integer - uptime: - type: integer - type: object - station.stationInfo: - properties: - cpu: - $ref: '#/definitions/station.cpuInfo' - disk: - $ref: '#/definitions/station.diskInfo' - explorer: - $ref: '#/definitions/station.explorerInfo' - memory: - $ref: '#/definitions/station.memoryInfo' - os: - $ref: '#/definitions/station.osInfo' - sensor: - $ref: '#/definitions/config.Sensor' - station: - $ref: '#/definitions/config.Station' - stream: - $ref: '#/definitions/config.Stream' - type: object info: contact: {} - description: This is APIv1 documentation for AnyShake Observer, please set `server_settings.debug` + description: This is API v1 documentation for AnyShake Observer, please set `server_settings.debug` to `false` in `config.json` when deploying to production environment in case of any security issues. - title: AnyShake Observer APIv1 + title: AnyShake Observer API v1 paths: + /auth: + post: + description: In restricted mode, the client must log in to access other APIs. + This API is used to checks the server's authentication status, issues an RSA + public key for credential encryption, generates a captcha, authenticates the + client, and signs or refreshes the JWT token. This API requires a valid JWT + token if action is `refresh`. + parameters: + - description: Specifies the action to be performed. Use `inspect` to check + the server's restriction status, `preauth` to get a Base64 RSA public key + in PEM format and generate a Base64 captcha PNG image, `login` to authenticate + the client using encrypted credentials, and `refresh` to refresh the JWT + token. + in: formData + name: action + required: true + type: string + - description: A unique string used to prevent replay attacks, required for + the `login` action and left empty for other actions. The nonce is the SHA-1 + hash of the RSA public key from the pre-authentication stage and becomes + invalid once the request is sent. It also expires if unused within the time-to-live + (TTL) period, which is set during the pre-authentication stage. + in: formData + name: nonce + type: string + - description: 'Base64 encrypted credential using the RSA public key, required + for the `login` action and left empty for other actions. The decrypted credential + is a JSON object that includes the username, password, captcha ID, and captcha + solution. Example: `{ username: admin, password: admin, captcha_id: 123, + captcha_solution: abc }`.' + in: formData + name: credential + type: string + - description: Bearer JWT token, only required for the `refresh` action. + in: header + name: Authorization + type: string + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: User Authentication /history: post: - consumes: - - application/x-www-form-urlencoded - description: Get waveform count data in specified time range, channel and format, - the maximum duration of the waveform data to be exported is 24 hours for JSON - and 1 hour for SAC + description: Get seismic waveform data from database in specified time range, + channel and format. This API supports 1 hour of maximum duration of the waveform + data to be queried. This API requires a valid JWT token if the server is in + restricted mode. parameters: - - description: Start timestamp of the waveform data to be queried, in milliseconds - (unix timestamp) + - description: Start time of the waveform to be queried, unix timestamp format + in milliseconds. in: formData name: start_time required: true type: integer - - description: End timestamp of the waveform data to be queried, in milliseconds - (unix timestamp) + - description: End time of the waveform to be queried, unix timestamp format + in milliseconds. in: formData name: end_time required: true type: integer - - description: Format of the waveform data to be queried, `json`, `sac` or `miniseed` + - description: Set output format of the waveform data, available options are + `json`, `sac`, and `miniseed`. in: formData name: format required: true type: string - - description: Channel of the waveform, `Z`, `E` or `N`, reuqired when format - is `sac` and `miniseed` + - description: Channel of the waveform, available options are `Z`, `E` or `N` + (in uppercase), only reuqired when output format is set to `sac` and `miniseed`. in: formData name: channel type: string + - description: Bearer JWT token, only required when the server is in restricted + mode. + in: header + name: Authorization + type: string produces: - application/json - application/octet-stream - responses: - "200": - description: Successfully exported the waveform data - schema: - allOf: - - $ref: '#/definitions/response.HttpResponse' - - properties: - data: - items: - $ref: '#/definitions/explorer.ExplorerData' - type: array - type: object - "400": - description: Failed to export waveform data due to invalid format or channel - schema: - $ref: '#/definitions/response.HttpResponse' - "410": - description: Failed to export waveform data due to no data available - schema: - $ref: '#/definitions/response.HttpResponse' - "500": - description: Failed to export waveform data due to failed to read data source - schema: - $ref: '#/definitions/response.HttpResponse' - summary: AnyShake Observer waveform history + responses: {} + security: + - ApiKeyAuth: [] + summary: Waveform History /inventory: get: - description: Get SeisComP XML inventory, which contains meta data of the station + description: Get SeisComP XML inventory, which contains meta data of the station. + This API requires a valid JWT token if the server is in restricted mode. parameters: - - description: Format of the inventory, either `json` or `xml` + - description: Format of the inventory, available options are `json` or `xml` in: query name: format type: string + - description: Bearer JWT token, only required when the server is in restricted + mode. + in: header + name: Authorization + type: string produces: - application/json - - application/xml - responses: - "200": - description: Successfully get SeisComP XML inventory - schema: - allOf: - - $ref: '#/definitions/response.HttpResponse' - - properties: - data: - type: string - type: object - summary: AnyShake Observer station inventory + - ' application/xml' + responses: {} + security: + - ApiKeyAuth: [] + summary: Station Inventory /mseed: post: - consumes: - - application/x-www-form-urlencoded - description: List MiniSEED data if action is `show`, or export MiniSEED data - in .mseed format if action is `export` + description: This API returns a list of MiniSEED files or exports a specific + MiniSEED file. This API requires a valid JWT token if the server is in restricted + mode. parameters: - - description: Action to be performed, either `show` or `export` + - description: Action to be performed, Use `list` to get list of MiniSEED files, + `export` to export a specific MiniSEED file. in: formData name: action required: true type: string - - description: Name of MiniSEED file to be exported, end with `.mseed` + - description: A valid filename of the MiniSEED file to be exported, only required + when action is `export`. in: formData name: name type: string + - description: Bearer JWT token, only required when the server is in restricted + mode. + in: header + name: Authorization + type: string produces: - application/json - application/octet-stream - responses: - "200": - description: Successfully get list of MiniSEED files - schema: - allOf: - - $ref: '#/definitions/response.HttpResponse' - - properties: - data: - items: - $ref: '#/definitions/mseed.miniSeedFileInfo' - type: array - type: object - "400": - description: Failed to list or export MiniSEED data due to invalid request - body - schema: - $ref: '#/definitions/response.HttpResponse' - "410": - description: Failed to export MiniSEED data due to invalid file name or - permission denied - schema: - $ref: '#/definitions/response.HttpResponse' - "500": - description: Failed to list or export MiniSEED data due to internal server - error - schema: - $ref: '#/definitions/response.HttpResponse' - summary: AnyShake Observer MiniSEED data + responses: {} + security: + - ApiKeyAuth: [] + summary: MiniSEED Data /station: get: description: Get Observer station status including system information, memory usage, disk usage, CPU usage, ADC information, geophone information, and location - information + information. This API requires a valid JWT token if the server is in restricted + mode. + parameters: + - description: Bearer JWT token, only required when the server is in restricted + mode. + in: header + name: Authorization + type: string produces: - application/json - responses: - "200": - description: Successfully read station information - schema: - allOf: - - $ref: '#/definitions/response.HttpResponse' - - properties: - data: - $ref: '#/definitions/station.stationInfo' - type: object - summary: AnyShake Observer station status + responses: {} + security: + - ApiKeyAuth: [] + summary: Station Status /trace: post: - consumes: - - application/x-www-form-urlencoded - description: Get list of earthquake events data source and earthquake events - from specified data source + description: This API retrieves seismic events from the specified data source, + including essential information such as event time, location, magnitude, depth + and estimated distance and arrival time from the station. This API requires + a valid JWT token if the server is in restricted mode. parameters: - - description: Use `show` to get available sources first, then choose one and + - description: Use `list` to get available sources first, then choose one and request again to get events in: formData name: source required: true type: string + - description: Bearer JWT token, only required when the server is in restricted + mode. + in: header + name: Authorization + type: string + produces: + - application/json + responses: {} + security: + - ApiKeyAuth: [] + summary: Seismic Trace + /user: + post: + description: This API is used to manage user accounts, including creating, removing, + and editing user profiles. This API only available in restricted mode and + requires a valid JWT token. + parameters: + - description: Specifies the action to be performed. Use `preauth` to get a + Base64 RSA public key in PEM format, `profile` to get profile of current + user, `list` to get list of all users (admin only), `create` to create a + new user (admin only), `remove` to remove a user (admin only), and `edit` + to edit a user (admin only). + in: formData + name: action + required: true + type: string + - description: A unique string used to prevent replay attacks, required for + the `create`, `remove`, `edit` actions and left empty for other actions. + The nonce is the SHA-1 hash of the RSA public key from the pre-authentication + stage and becomes invalid once the request is sent. It also expires if unused + within the time-to-live (TTL) period, which is set during the pre-authentication + stage. + in: formData + name: nonce + type: string + - description: The user ID to be removed or edited, required for the `remove` + and `edit` actions and left empty for other actions. The user ID is encrypted + with the RSA public key. + in: formData + name: user_id + type: string + - description: Specifies whether the user is an administrator, required for + the `create` and `edit` actions and set to false in other actions. + in: formData + name: admin + type: boolean + - description: The username of the user to be created or edited, required for + the `create` and `edit` actions and left empty for other actions. The username + is encrypted with the RSA public key. + in: formData + name: username + type: string + - description: The password of the user to be created or edited, required for + the `create` and `edit` actions and left empty for other actions. The password + is encrypted with the RSA public key. + in: formData + name: password + type: string + - description: Bearer JWT token. + in: header + name: Authorization + required: true + type: string produces: - application/json - responses: - "200": - description: Successfully read the list of earthquake events - schema: - allOf: - - $ref: '#/definitions/response.HttpResponse' - - properties: - data: - items: - $ref: '#/definitions/seisevent.Event' - type: array - type: object - "400": - description: Failed to read earthquake event list due to invalid data source - schema: - $ref: '#/definitions/response.HttpResponse' - "500": - description: Failed to read earthquake event list due to failed to read - data source - schema: - $ref: '#/definitions/response.HttpResponse' - summary: AnyShake Observer event trace + responses: {} + security: + - ApiKeyAuth: [] + summary: User Management swagger: "2.0" diff --git a/drivers/dao/tables/adc_count.go b/drivers/dao/tables/adc_count.go index 2f77419e..dd5f79ee 100644 --- a/drivers/dao/tables/adc_count.go +++ b/drivers/dao/tables/adc_count.go @@ -15,7 +15,7 @@ type AdcCount struct { } func (t AdcCount) GetModel() any { - return t + return &AdcCount{} } func (t AdcCount) GetName() string { diff --git a/drivers/dao/tables/sys_user.go b/drivers/dao/tables/sys_user.go new file mode 100644 index 00000000..e3845ef9 --- /dev/null +++ b/drivers/dao/tables/sys_user.go @@ -0,0 +1,42 @@ +package tables + +import ( + "github.com/anyshake/observer/drivers/dao" + "github.com/bwmarrin/snowflake" + "golang.org/x/crypto/bcrypt" +) + +type SysUser struct { + dao.BaseTable + UserId int64 `gorm:"column:user_id;index;not null;unique"` // Unique user ID generated using Snowflake + Username string `gorm:"column:username;index;not null;unique"` + Password string `gorm:"column:password;not null"` // Must be hashed using GetHashedPassword + LastLogin int64 `gorm:"column:last_login"` + UserIp string `gorm:"column:user_ip"` + UserAgent string `gorm:"column:user_agent"` + Admin string `gorm:"column:admin;index;not null;default:false"` // false or true in string + UpdatedAt int64 `gorm:"column:update_at;autoUpdateTime:milli;<-:update"` +} + +func (t SysUser) GetModel() any { + return &SysUser{} +} + +func (t SysUser) GetName() string { + return "sys_user" +} + +func (t SysUser) NewUserId() int64 { + id, _ := snowflake.NewNode(1) + return id.Generate().Int64() +} + +func (t SysUser) GetHashedPassword(password string) string { + hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hashedPassword) +} + +func (t SysUser) IsPasswordCorrect(password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(t.Password), []byte(password)) + return err == nil +} diff --git a/drivers/explorer/impl.go b/drivers/explorer/impl.go index 54480575..6e6ed6f6 100644 --- a/drivers/explorer/impl.go +++ b/drivers/explorer/impl.go @@ -11,6 +11,7 @@ import ( "github.com/alphadose/haxmap" "github.com/anyshake/observer/utils/fifo" + "github.com/sirupsen/logrus" messagebus "github.com/vardius/message-bus" ) @@ -137,7 +138,7 @@ func (g *mainlinePacket) decode(data []byte) error { } type ExplorerDriverImpl struct { - logger ExplorerLogger + logger *logrus.Entry legacyPacket legacyPacket mainlinePacket mainlinePacket } @@ -224,7 +225,7 @@ func (e *ExplorerDriverImpl) handleReadLegacyPacket(deps *ExplorerDependency, fi // Read the packet data err = e.legacyPacket.decode(dat[len(LEGACY_PACKET_FRAME_HEADER):]) if err != nil { - e.logger.Warnf("failed to decode legacy packet: %v", err) + e.logger.Errorf("failed to decode legacy packet: %v", err) deps.Health.SetErrors(deps.Health.GetErrors() + 1) } else { packetBuffer = append(packetBuffer, e.legacyPacket) @@ -284,7 +285,7 @@ func (e *ExplorerDriverImpl) handleReadMainlinePacket(deps *ExplorerDependency, } err = e.mainlinePacket.decode(dat[len(MAINLINE_PACKET_FRAME_HEADER):]) if err != nil { - e.logger.Warnf("failed to decode mainline packet: %v", err) + e.logger.Errorf("failed to decode mainline packet: %v", err) deps.Health.SetErrors(deps.Health.GetErrors() + 1) continue } @@ -382,7 +383,7 @@ func (e *ExplorerDriverImpl) readerDaemon(deps *ExplorerDependency) { } } -func (e *ExplorerDriverImpl) Init(deps *ExplorerDependency, logger ExplorerLogger) error { +func (e *ExplorerDriverImpl) Init(deps *ExplorerDependency, logger *logrus.Entry) error { e.logger = logger currentTime := deps.FallbackTime.Get() diff --git a/drivers/explorer/types.go b/drivers/explorer/types.go index bc307541..cff3777f 100644 --- a/drivers/explorer/types.go +++ b/drivers/explorer/types.go @@ -8,6 +8,7 @@ import ( "github.com/alphadose/haxmap" "github.com/anyshake/observer/drivers/transport" "github.com/anyshake/observer/utils/timesource" + "github.com/sirupsen/logrus" messagebus "github.com/vardius/message-bus" ) @@ -57,15 +58,9 @@ type ExplorerData struct { type ExplorerEventHandler = func(data *ExplorerData) -type ExplorerLogger interface { - Infof(format string, args ...any) - Warnf(format string, args ...any) - Errorf(format string, args ...any) -} - type ExplorerDriver interface { readerDaemon(deps *ExplorerDependency) - Init(deps *ExplorerDependency, logger ExplorerLogger) error + Init(deps *ExplorerDependency, logger *logrus.Entry) error Subscribe(deps *ExplorerDependency, clientId string, handler ExplorerEventHandler) error Unsubscribe(deps *ExplorerDependency, clientId string) error } diff --git a/frontend/src/craco.config.ts b/frontend/src/craco.config.ts index f5ba09c1..46e186e5 100644 --- a/frontend/src/craco.config.ts +++ b/frontend/src/craco.config.ts @@ -26,7 +26,7 @@ module.exports = { } catch { version = "custombuild"; } - return JSON.stringify(`${version}-${commit}-${Date.now() / 1000}`); + return JSON.stringify(`${version}-${commit}-${Math.floor(Date.now() / 1000)}`); }; webpackConfig.plugins!.push( new DefinePlugin({ diff --git a/frontend/src/package-lock.json b/frontend/src/package-lock.json index 66a9e197..503cff9e 100644 --- a/frontend/src/package-lock.json +++ b/frontend/src/package-lock.json @@ -12,16 +12,19 @@ "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", "@mui/material": "^5.14.14", + "@mui/x-data-grid": "^7.17.0", "@mui/x-date-pickers": "^6.16.0", "@reduxjs/toolkit": "^1.9.6", "axios": "^1.5.1", "date-fns": "^2.30.0", "file-saver": "^2.0.5", + "formik": "^2.4.6", "highcharts": "^11.1.0", "highcharts-react-official": "^3.2.1", "i18next": "^23.5.1", "i18next-browser-languagedetector": "^7.1.0", "leaflet": "^1.9.4", + "node-forge": "^1.3.1", "oregondsp": "^1.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -2136,9 +2139,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "version": "7.25.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.25.6.tgz", + "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3931,11 +3934,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.14", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", - "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", + "version": "7.2.16", + "resolved": "https://registry.npmmirror.com/@mui/types/-/types-7.2.16.tgz", + "integrity": "sha512-qI8TV3M7ShITEEc8Ih15A2vLzZGLhD+/UPNwck/hcls2gwg7dyRjNGXcQYHKLB5Q7PuTRfrTkAoPa2VV1s67Ag==", "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0" + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3944,14 +3947,16 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.14", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", - "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", + "version": "5.16.6", + "resolved": "https://registry.npmmirror.com/@mui/utils/-/utils-5.16.6.tgz", + "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@types/prop-types": "^15.7.11", + "@mui/types": "^7.2.15", + "@types/prop-types": "^15.7.12", + "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^18.2.0" + "react-is": "^18.3.1" }, "engines": { "node": ">=12.0.0" @@ -3971,9 +3976,50 @@ } }, "node_modules/@mui/utils/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/@mui/x-data-grid": { + "version": "7.17.0", + "resolved": "https://registry.npmmirror.com/@mui/x-data-grid/-/x-data-grid-7.17.0.tgz", + "integrity": "sha512-d3pFdrQlNR+8waol7iM6LlNIpvqo9SgYeKcMIOSQ3etpue9iRFNy8s1HCHd9Nxnhzgr+fqMy/v3bXZnd196qig==", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^5.16.6", + "@mui/x-internals": "7.17.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^5.1.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" }, "node_modules/@mui/x-date-pickers": { "version": "6.19.8", @@ -4040,6 +4086,25 @@ } } }, + "node_modules/@mui/x-internals": { + "version": "7.17.0", + "resolved": "https://registry.npmmirror.com/@mui/x-internals/-/x-internals-7.17.0.tgz", + "integrity": "sha512-FLlAGSJl/vsuaA/8hPGazXFppyzIzxApJJDZMoTS0geUmHd0hyooISV2ltllLmrZ/DGtHhI08m8GGnHL6/vVeg==", + "dependencies": { + "@babel/runtime": "^7.25.6", + "@mui/utils": "^5.16.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -4866,9 +4931,9 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + "version": "15.7.12", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/q": { "version": "1.5.8", @@ -6888,9 +6953,9 @@ } }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -9752,6 +9817,38 @@ "node": ">=0.4.x" } }, + "node_modules/formik": { + "version": "2.4.6", + "resolved": "https://registry.npmmirror.com/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13844,8 +13941,12 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -14295,9 +14396,8 @@ }, "node_modules/node-forge": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, "engines": { "node": ">= 6.13.0" } @@ -16755,6 +16855,11 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "dev": true }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "node_modules/react-hot-toast": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", @@ -18938,6 +19043,11 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -19115,8 +19225,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/frontend/src/package.json b/frontend/src/package.json index 9428d696..42646bdb 100644 --- a/frontend/src/package.json +++ b/frontend/src/package.json @@ -8,16 +8,19 @@ "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", "@mui/material": "^5.14.14", + "@mui/x-data-grid": "^7.17.0", "@mui/x-date-pickers": "^6.16.0", "@reduxjs/toolkit": "^1.9.6", "axios": "^1.5.1", "date-fns": "^2.30.0", "file-saver": "^2.0.5", + "formik": "^2.4.6", "highcharts": "^11.1.0", "highcharts-react-official": "^3.2.1", "i18next": "^23.5.1", "i18next-browser-languagedetector": "^7.1.0", "leaflet": "^1.9.4", + "node-forge": "^1.3.1", "oregondsp": "^1.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/src/src/App.tsx b/frontend/src/src/App.tsx index cdaec210..4c811e66 100644 --- a/frontend/src/src/App.tsx +++ b/frontend/src/src/App.tsx @@ -1,132 +1,111 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import { useLocation } from "react-router-dom"; import { Container } from "./components/Container"; -import { Footer } from "./components/Footer"; -import { Header } from "./components/Header"; -import { Navbar } from "./components/Navbar"; -import { RouterView } from "./components/RouterView"; -import { Scroller } from "./components/Scroller"; -import { Sidebar } from "./components/Sidebar"; -import { Skeleton } from "./components/Skeleton"; -import { apiConfig } from "./config/api"; -import { globalConfig } from "./config/global"; +import { apiConfig, authCommonResponseModel1 } from "./config/api"; import i18n, { i18nConfig } from "./config/i18n"; -import { menuConfig } from "./config/menu"; -import { routerConfig } from "./config/router"; import { hideLoading } from "./helpers/app/hideLoading"; import { getCurrentLocale } from "./helpers/i18n/getCurrentLocale"; import { setUserLocale } from "./helpers/i18n/setUserLocale"; import { requestRestApi } from "./helpers/request/requestRestApi"; -import { onUpdate as UpdateADC } from "./stores/adc"; -import { onUpdate as UpdateGeophone } from "./stores/geophone"; -import { onUpdate as UpdateStation } from "./stores/station"; +import { Login } from "./Login"; +import { Main } from "./Main"; +import { initialCredential, onUpdate as UpdateCredential } from "./stores/credential"; const App = () => { - const { t } = useTranslation(); - const { routes, basename } = routerConfig; - const { fallback, resources } = i18nConfig; - const { name, title, author, repository, homepage, footer } = globalConfig; - - useEffect(() => { - hideLoading(); - // eslint-disable-next-line no-console - console.log(`%c${process.env.BUILD_TAG ?? "custom build"}`, "color: #0369a1;"); - }, []); - - const { pathname } = useLocation(); - const [currentTitle, setCurrentTitle] = useState(title); - const [currentLocale, setCurrentLocale] = useState(fallback); - - const setCurrentLocaleToState = async () => { - setCurrentLocale(await getCurrentLocale(i18n)); - }; - - const getCurrentTitle = useCallback(() => { - for (const key in routes) { - const { prefix, uri, suffix } = routes[key]; - const fullPath = `${prefix}${uri}${suffix}`; - if (pathname === fullPath) { - return routes[key].title[currentLocale]; - } - } - return routes.default.title[currentLocale]; - }, [routes, pathname, currentLocale]); - - useEffect(() => { - void setCurrentLocaleToState(); - const subtitle = getCurrentTitle(); - setCurrentTitle(subtitle); - document.title = `${subtitle} - ${title}`; - }, [t, getCurrentTitle, title]); - + // Check if the user needs to login before loading the main page + const [appInspection, setAppInspection] = useState({ + hasRestrict: false, + hasLoggedIn: false, + loadPage: false + }); const dispatch = useDispatch(); - - const getStationAttributes = useCallback(async () => { + const getAppInspection = useCallback(async () => { const { backend, endpoints } = apiConfig; - const res = await requestRestApi({ + const res = (await requestRestApi({ backend, - timeout: 30, - endpoint: endpoints.station - }); - if (res?.data) { - const initialized = true; - const { sensitivity, frequency } = res.data.sensor; - dispatch(UpdateGeophone({ sensitivity, frequency, initialized })); - const { resolution, fullscale } = res.data.sensor; - dispatch(UpdateADC({ resolution, fullscale, initialized })); - const { station, network, location, channel } = res.data.stream; - dispatch(UpdateStation({ station, network, location, initialized, channel })); + endpoint: endpoints.auth, + payload: { action: "inspect", nonce: "", credential: "" } + })) as typeof authCommonResponseModel1; + if (res.data) { + // Clear credential store if not restricted + if (!res.data?.restrict) { + dispatch(UpdateCredential(initialCredential)); + } + setAppInspection({ + hasLoggedIn: !(res.data?.restrict ?? false), + hasRestrict: res.data?.restrict, + loadPage: true + }); } }, [dispatch]); + useEffect(() => { + getAppInspection(); + }, [getAppInspection]); + // Remove spinner after inspection useEffect(() => { - void getStationAttributes(); - }, [getStationAttributes]); + if (appInspection.loadPage && appInspection.hasLoggedIn) { + // eslint-disable-next-line no-console + console.log(`%c${process.env.BUILD_TAG ?? "custom build"}`, "color: #0369a1;"); + } + if (appInspection.loadPage) { + hideLoading(); + } + }, [appInspection]); - const handleSwitchLocale = (locale: string) => { - void setUserLocale(i18n, locale); + // Handler for login state change + const handleLoginStateChange = (alive: boolean) => { + if (!alive) { + dispatch(UpdateCredential(initialCredential)); + } + setAppInspection({ ...appInspection, hasLoggedIn: alive, loadPage: true }); }; - const locales = Object.entries(resources).reduce( + // Get current locale from i18n + const [currentLocale, setCurrentLocale] = useState(i18nConfig.fallback); + const { t } = useTranslation(); + useEffect(() => { + const setCurrentLocaleToState = async () => { + setCurrentLocale(await getCurrentLocale(i18n)); + }; + setCurrentLocaleToState(); + }, [t]); + + // Locale resources and switcher + const locales = Object.entries(i18nConfig.resources).reduce( (acc, [key, value]) => { acc[key] = value.label; return acc; }, {} as Record ); + const handleSwitchLocale = (newLocale: string) => { + setUserLocale(i18n, newLocale); + }; return ( -
- - - - - } - routerProps={{ - locale: currentLocale - }} - /> - - - -