diff --git a/api/internal/db/batch.go b/api/internal/db/batch.go index d0e290df..8bac8a3d 100644 --- a/api/internal/db/batch.go +++ b/api/internal/db/batch.go @@ -1309,7 +1309,7 @@ func (b *IpiOptsCreateBatchBatchResults) Close() error { return b.br.Close() } -const ipiOptsUpdateBatch = `-- name: IpiOptsUpdateBatch :batchexec +const ipiOptsUpdateBatch = `-- name: IpiOptsUpdateBatch :batchone update ipi_opts set initial_time = $2 where instrument_id = $1 @@ -1340,18 +1340,20 @@ func (q *Queries) IpiOptsUpdateBatch(ctx context.Context, arg []IpiOptsUpdateBat return &IpiOptsUpdateBatchBatchResults{br, len(arg), false} } -func (b *IpiOptsUpdateBatchBatchResults) Exec(f func(int, error)) { +func (b *IpiOptsUpdateBatchBatchResults) QueryRow(f func(int, uuid.UUID, error)) { defer b.br.Close() for t := 0; t < b.tot; t++ { + var bottom_elevation_timeseries_id uuid.UUID if b.closed { if f != nil { - f(t, ErrBatchAlreadyClosed) + f(t, bottom_elevation_timeseries_id, ErrBatchAlreadyClosed) } continue } - _, err := b.br.Exec() + row := b.br.QueryRow() + err := row.Scan(&bottom_elevation_timeseries_id) if f != nil { - f(t, err) + f(t, bottom_elevation_timeseries_id, err) } } } @@ -2145,7 +2147,7 @@ func (b *SaaOptsCreateBatchBatchResults) Close() error { return b.br.Close() } -const saaOptsUpdateBatch = `-- name: SaaOptsUpdateBatch :batchexec +const saaOptsUpdateBatch = `-- name: SaaOptsUpdateBatch :batchone update saa_opts set initial_time = $2 where instrument_id = $1 @@ -2176,18 +2178,20 @@ func (q *Queries) SaaOptsUpdateBatch(ctx context.Context, arg []SaaOptsUpdateBat return &SaaOptsUpdateBatchBatchResults{br, len(arg), false} } -func (b *SaaOptsUpdateBatchBatchResults) Exec(f func(int, error)) { +func (b *SaaOptsUpdateBatchBatchResults) QueryRow(f func(int, uuid.UUID, error)) { defer b.br.Close() for t := 0; t < b.tot; t++ { + var bottom_elevation_timeseries_id uuid.UUID if b.closed { if f != nil { - f(t, ErrBatchAlreadyClosed) + f(t, bottom_elevation_timeseries_id, ErrBatchAlreadyClosed) } continue } - _, err := b.br.Exec() + row := b.br.QueryRow() + err := row.Scan(&bottom_elevation_timeseries_id) if f != nil { - f(t, err) + f(t, bottom_elevation_timeseries_id, err) } } } diff --git a/api/internal/dto/instrument.go b/api/internal/dto/instrument.go index 184cc2c0..aa009b34 100644 --- a/api/internal/dto/instrument.go +++ b/api/internal/dto/instrument.go @@ -26,14 +26,20 @@ type InstrumentDTO struct { ShowCwmsTab bool `json:"show_cwms_tab" required:"false"` RawOpts json.RawMessage `json:"opts" required:"false"` Opts struct { - SaaOpts SaaOptsDTO - IpiOpts IpiOptsDTO - InclOpts InclOptsDTO + SaaOpts DepthOptsDTO + IpiOpts DepthOptsDTO + InclOpts DepthOptsDTO SeisOpts SeisOptsDTO } `json:"-"` AuditInfo } +type DepthOptsDTO struct { + NumSegments int `json:"num_segments"` + BottomElevation float64 `json:"bottom_elevation"` + InitialTime *time.Time `json:"initial_time" required:"false"` +} + func (d *InstrumentDTO) Resolve(ctx huma.Context, prefix *huma.PathBuffer) []error { var err error switch d.TypeID { diff --git a/api/internal/dto/instrument_incl.go b/api/internal/dto/instrument_incl.go index e942131f..f120fcbd 100644 --- a/api/internal/dto/instrument_incl.go +++ b/api/internal/dto/instrument_incl.go @@ -1,17 +1,9 @@ package dto import ( - "time" - "github.com/google/uuid" ) -type InclOptsDTO struct { - NumSegments int `json:"num_segments"` - BottomElevation float64 `json:"bottom_elevation"` - InitialTime *time.Time `json:"initial_time" required:"false"` -} - type InclSegmentDTO struct { ID int `json:"id"` DepthTimeseriesID *uuid.UUID `json:"depth_timeseries_id" required:"false"` diff --git a/api/internal/dto/instrument_ipi.go b/api/internal/dto/instrument_ipi.go index bcf2c345..4493aa00 100644 --- a/api/internal/dto/instrument_ipi.go +++ b/api/internal/dto/instrument_ipi.go @@ -1,18 +1,9 @@ package dto import ( - "time" - "github.com/google/uuid" ) -type IpiOptsDTO struct { - NumSegments int `json:"num_segments"` - BottomElevationTimeseriesID uuid.UUID `json:"bottom_elevation_timeseries_id"` - BottomElevation float64 `json:"bottom_elevation"` - InitialTime *time.Time `json:"initial_time" required:"false"` -} - type IpiSegmentDTO struct { ID int `json:"id"` Length *float64 `json:"length" required:"false"` diff --git a/api/internal/dto/instrument_saa.go b/api/internal/dto/instrument_saa.go index 56eb7b1c..c2efe592 100644 --- a/api/internal/dto/instrument_saa.go +++ b/api/internal/dto/instrument_saa.go @@ -1,18 +1,9 @@ package dto import ( - "time" - "github.com/google/uuid" ) -type SaaOptsDTO struct { - NumSegments int `json:"num_segments"` - BottomElevationTimeseriesID uuid.UUID `json:"bottom_elevation_timeseries_id"` - BottomElevation float64 `json:"bottom_elevation"` - InitialTime *time.Time `json:"initial_time" required:"false"` -} - type SaaSegmentDTO struct { ID int `json:"id"` Length *float64 `json:"length" required:"false"` diff --git a/api/internal/handler/instrument.go b/api/internal/handler/instrument.go index 760c14f9..901403b7 100644 --- a/api/internal/handler/instrument.go +++ b/api/internal/handler/instrument.go @@ -69,9 +69,7 @@ var ( instrumentDTOType = reflect.TypeFor[dto.InstrumentDTO]() vInstrumentType = reflect.TypeFor[[]db.VInstrument]() featureCollectionType = reflect.TypeFor[geojson.FeatureCollection]() - saaOptsDTOType = reflect.TypeFor[dto.SaaOptsDTO]() - ipiOptsDTOType = reflect.TypeFor[dto.IpiOptsDTO]() - inclOptsDTOType = reflect.TypeFor[dto.InclOptsDTO]() + depthOptsDTOType = reflect.TypeFor[dto.DepthOptsDTO]() seisOptsDTOType = reflect.TypeFor[dto.SeisOptsDTO]() instrumentCreateBatchRowType = reflect.TypeFor[db.InstrumentCreateBatchRow]() instrumentsValidationType = reflect.TypeFor[service.InstrumentsValidation]() @@ -401,10 +399,8 @@ func (h *ApiHandler) RegisterInstrument(api huma.API) { }) instrumentOptsDTOSchema := &huma.Schema{ - AnyOf: []*huma.Schema{ - registry.Schema(saaOptsDTOType, true, ""), - registry.Schema(ipiOptsDTOType, true, ""), - registry.Schema(inclOptsDTOType, true, ""), + OneOf: []*huma.Schema{ + registry.Schema(depthOptsDTOType, true, ""), registry.Schema(seisOptsDTOType, true, ""), }, Nullable: true, diff --git a/api/internal/middleware/key.go b/api/internal/middleware/key.go index e539b9ec..217bb654 100644 --- a/api/internal/middleware/key.go +++ b/api/internal/middleware/key.go @@ -2,6 +2,7 @@ package middleware import ( "context" + "crypto/subtle" "errors" "github.com/USACE/instrumentation-api/api/v4/internal/ctxkey" @@ -13,10 +14,12 @@ import ( // AppKeyAuth does a key check for ?key= func (m *apiMw) AppKeyAuth(ctx huma.Context, next func(huma.Context)) { provided := ctx.Query("key") - if provided == m.Config.ApplicationKey && provided != "" { - newCtx := context.WithValue(ctx.Context(), ctxkey.AppKeyAuthSuccess, true) - next(huma.WithContext(ctx, newCtx)) - return + if provided != "" { + if res := subtle.ConstantTimeCompare([]byte(provided), []byte(m.Config.ApplicationKey)); res == 1 { + newCtx := context.WithValue(ctx.Context(), ctxkey.AppKeyAuthSuccess, true) + next(huma.WithContext(ctx, newCtx)) + return + } } m.httperr.SetResponse(ctx, httperr.Unauthorized(errors.New("Unauthorized: invalid or missing key"))) @@ -32,7 +35,7 @@ func (m *apiMw) keyAuth(isDisabled bool, appKey string, h HashExtractorFunc) fun return } - if providedKey == appKey { + if res := subtle.ConstantTimeCompare([]byte(providedKey), []byte(appKey)); res == 1 { newCtx := context.WithValue(ctx.Context(), ctxkey.AppKeyAuthSuccess, true) next(huma.WithContext(ctx, newCtx)) return diff --git a/api/internal/service/instrument_incl.go b/api/internal/service/instrument_incl.go index e06a776b..3fa6617e 100644 --- a/api/internal/service/instrument_incl.go +++ b/api/internal/service/instrument_incl.go @@ -116,10 +116,9 @@ func createInclOptsBatch(ctx context.Context, q *db.Queries, ii []dto.Instrument func updateInclOptsBatch(ctx context.Context, q *db.Queries, ii []dto.InstrumentDTO) error { updateInclOptsParams := make([]db.InclOptsUpdateBatchParams, len(ii)) for idx, inst := range ii { - opts := inst.Opts.InclOpts updateInclOptsParams[idx] = db.InclOptsUpdateBatchParams{ InstrumentID: inst.ID, - InitialTime: opts.InitialTime, + InitialTime: inst.Opts.InclOpts.InitialTime, } } var err error diff --git a/api/internal/service/instrument_ipi.go b/api/internal/service/instrument_ipi.go index faf0f73b..6f40bd97 100644 --- a/api/internal/service/instrument_ipi.go +++ b/api/internal/service/instrument_ipi.go @@ -60,7 +60,7 @@ func createIpiOptsBatch(ctx context.Context, q *db.Queries, ii []dto.InstrumentD createBottomElevationMmtParams := make([]db.TimeseriesMeasurementCreateBatchParams, len(ii)) for idx, inst := range ii { - opts := inst.Opts.InclOpts + opts := inst.Opts.IpiOpts createTimeseriesBatchParams[idx] = make([]db.TimeseriesCreateBatchParams, opts.NumSegments) createIpiSegmentBatchParams[idx] = make([]db.IpiSegmentCreateBatchParams, opts.NumSegments) @@ -151,17 +151,20 @@ func updateIpiOptsBatch(ctx context.Context, q *db.Queries, ii []dto.InstrumentD InstrumentID: inst.ID, InitialTime: opts.InitialTime, } - createMmtParams[idx] = db.TimeseriesMeasurementCreateBatchParams{ - TimeseriesID: opts.BottomElevationTimeseriesID, - Time: time.Now(), - Value: opts.BottomElevation, - } } var err error - q.IpiOptsUpdateBatch(ctx, updateIpiOptsParams).Exec(batchExecErr(&err)) + tsIDs := make([]uuid.UUID, len(ii)) + q.IpiOptsUpdateBatch(ctx, updateIpiOptsParams).QueryRow(batchQueryRowCollect(tsIDs, &err)) if err != nil { return err } + for idx, inst := range ii { + createMmtParams[idx] = db.TimeseriesMeasurementCreateBatchParams{ + TimeseriesID: tsIDs[idx], + Time: time.Now(), + Value: inst.Opts.IpiOpts.BottomElevation, + } + } q.TimeseriesMeasurementCreateBatch(ctx, createMmtParams).Exec(batchExecErr(&err)) return err } diff --git a/api/internal/service/instrument_saa.go b/api/internal/service/instrument_saa.go index 33effcde..21fce8fa 100644 --- a/api/internal/service/instrument_saa.go +++ b/api/internal/service/instrument_saa.go @@ -136,22 +136,24 @@ func updateSaaOptsBatch(ctx context.Context, q *db.Queries, ii []dto.InstrumentD updateSaaOptsParams := make([]db.SaaOptsUpdateBatchParams, len(ii)) createMmtParams := make([]db.TimeseriesMeasurementCreateBatchParams, len(ii)) for idx, inst := range ii { - opts := inst.Opts.SaaOpts updateSaaOptsParams[idx] = db.SaaOptsUpdateBatchParams{ InstrumentID: inst.ID, - InitialTime: opts.InitialTime, - } - createMmtParams[idx] = db.TimeseriesMeasurementCreateBatchParams{ - TimeseriesID: opts.BottomElevationTimeseriesID, - Time: time.Now(), - Value: opts.BottomElevation, + InitialTime: inst.Opts.SaaOpts.InitialTime, } } var err error - q.SaaOptsUpdateBatch(ctx, updateSaaOptsParams).Exec(batchExecErr(&err)) + tsIDs := make([]uuid.UUID, len(ii)) + q.SaaOptsUpdateBatch(ctx, updateSaaOptsParams).QueryRow(batchQueryRowCollect(tsIDs, &err)) if err != nil { return err } + for idx, inst := range ii { + createMmtParams[idx] = db.TimeseriesMeasurementCreateBatchParams{ + TimeseriesID: tsIDs[idx], + Time: time.Now(), + Value: inst.Opts.SaaOpts.BottomElevation, + } + } q.TimeseriesMeasurementCreateBatch(ctx, createMmtParams).Exec(batchExecErr(&err)) return err } diff --git a/api/queries/instrument_ipi.sql b/api/queries/instrument_ipi.sql index e9bed420..60c0af98 100644 --- a/api/queries/instrument_ipi.sql +++ b/api/queries/instrument_ipi.sql @@ -3,7 +3,7 @@ insert into ipi_opts (instrument_id, num_segments, bottom_elevation_timeseries_i values ($1, $2, $3, $4); --- name: IpiOptsUpdateBatch :batchexec +-- name: IpiOptsUpdateBatch :batchone update ipi_opts set initial_time = $2 where instrument_id = $1 diff --git a/api/queries/instrument_saa.sql b/api/queries/instrument_saa.sql index 919efed4..8091f2b5 100644 --- a/api/queries/instrument_saa.sql +++ b/api/queries/instrument_saa.sql @@ -3,7 +3,7 @@ insert into saa_opts (instrument_id, num_segments, bottom_elevation_timeseries_i values ($1, $2, $3, $4); --- name: SaaOptsUpdateBatch :batchexec +-- name: SaaOptsUpdateBatch :batchone update saa_opts set initial_time = $2 where instrument_id = $1 diff --git a/report/generated.d.ts b/report/generated.d.ts index 65b89533..f96cc4e1 100644 --- a/report/generated.d.ts +++ b/report/generated.d.ts @@ -643,6 +643,10 @@ export interface paths { /** @description deletes a survey123 */ delete: operations["survey123-delete"]; }; + "/projects/{project_id}/survey123/{survey123_id}/key": { + /** @description cycles a Survey123 key (project admin) */ + put: operations["survey123-update-key"]; + }; "/projects/{project_id}/survey123/{survey123_id}/previews": { /** @description gets a preview payload for a survey123 */ get: operations["survey123-get-preview"]; @@ -1071,6 +1075,14 @@ export interface components { updated_by: string; updated_by_username: string; }; + DepthOptsDTO: { + /** Format: double */ + bottom_elevation: number; + /** Format: date-time */ + initial_time?: string | null; + /** Format: int64 */ + num_segments: number; + }; DomainGroupOpt: { description: string | null; id: string; @@ -1266,14 +1278,6 @@ export interface components { /** Format: int64 */ segment_id: number; }; - InclOptsDTO: { - /** Format: double */ - bottom_elevation: number; - /** Format: date-time */ - initial_time?: string | null; - /** Format: int64 */ - num_segments: number; - }; InclSegmentDTO: { a0_timeseries_id?: string; a180_timeseries_id?: string; @@ -1324,7 +1328,7 @@ export interface components { nid_id?: string | null; /** Format: double */ offset?: number | null; - opts?: components["schemas"]["SaaOptsDTO"] | components["schemas"]["IpiOptsDTO"] | components["schemas"]["InclOptsDTO"] | components["schemas"]["SeisOptsDTO"] | Record | null; + opts?: components["schemas"]["DepthOptsDTO"] | components["schemas"]["SeisOptsDTO"] | Record | null; projects?: components["schemas"]["IDDTO"][] | null; show_cwms_tab?: boolean; /** Format: double */ @@ -1472,15 +1476,6 @@ export interface components { /** Format: double */ tilt: number | null; }; - IpiOptsDTO: { - /** Format: double */ - bottom_elevation: number; - bottom_elevation_timeseries_id: string; - /** Format: date-time */ - initial_time?: string | null; - /** Format: int64 */ - num_segments: number; - }; IpiSegmentDTO: { /** Format: int64 */ id: number; @@ -2034,15 +2029,6 @@ export interface components { /** Format: double */ z_increment: number | null; }; - SaaOptsDTO: { - /** Format: double */ - bottom_elevation: number; - bottom_elevation_timeseries_id: string; - /** Format: date-time */ - initial_time?: string | null; - /** Format: int64 */ - num_segments: number; - }; SaaSegmentDTO: { /** Format: int64 */ id: number; @@ -2104,7 +2090,6 @@ export interface components { Survey123EquivalencyTableField: { display_name: string; field_name: string; - instrument_id: string; timeseries_id: string; }; Survey123EquivalencyTableFieldDTO: { @@ -2112,6 +2097,15 @@ export interface components { field_name: string; timeseries_id?: string; }; + Survey123IDWithKey: { + /** + * Format: uri + * @description A URL to the JSON Schema for this object. + */ + $schema?: string; + id: string; + key: string; + }; Survey123Preview: { /** * Format: uri @@ -2123,6 +2117,12 @@ export interface components { /** Format: date-time */ updated_at: string; }; + Survey123TelemetryDTO: { + Key: string; + Payload: { + [key: string]: unknown; + }; + }; TelemetryPreview: { /** * Format: uri @@ -3454,7 +3454,6 @@ export interface operations { "measurement-list-for-instrument-list-explorer": { parameters: { query?: { - expertimental?: boolean; threshold?: number; time_window?: string; after?: string; @@ -3718,7 +3717,6 @@ export interface operations { "measurement-list-for-instrument-group": { parameters: { query?: { - expertimental?: boolean; threshold?: number; time_window?: string; after?: string; @@ -4395,7 +4393,6 @@ export interface operations { "measurement-list-for-instrument-timeseries": { parameters: { query?: { - expertimental?: boolean; threshold?: number; time_window?: string; after?: string; @@ -4432,7 +4429,6 @@ export interface operations { "measurement-list-for-instrument": { parameters: { query?: { - expertimental?: boolean; threshold?: number; time_window?: string; after?: string; @@ -5732,7 +5728,7 @@ export interface operations { nid_id?: string | null; /** Format: double */ offset?: number | null; - opts?: components["schemas"]["SaaOptsDTO"] | components["schemas"]["IpiOptsDTO"] | components["schemas"]["InclOptsDTO"] | components["schemas"]["SeisOptsDTO"] | Record | null; + opts?: components["schemas"]["DepthOptsDTO"] | components["schemas"]["SeisOptsDTO"] | Record | null; projects?: components["schemas"]["IDDTO"][] | null; show_cwms_tab?: boolean; /** Format: double */ @@ -5966,7 +5962,7 @@ export interface operations { nid_id?: string | null; /** Format: double */ offset?: number | null; - opts?: components["schemas"]["SaaOptsDTO"] | components["schemas"]["IpiOptsDTO"] | components["schemas"]["InclOptsDTO"] | components["schemas"]["SeisOptsDTO"] | Record | null; + opts?: components["schemas"]["DepthOptsDTO"] | components["schemas"]["SeisOptsDTO"] | Record | null; projects?: components["schemas"]["IDDTO"][] | null; show_cwms_tab?: boolean; /** Format: double */ @@ -7495,7 +7491,7 @@ export interface operations { /** @description Created */ 201: { content: { - "application/json": components["schemas"]["ID"]; + "application/json": components["schemas"]["Survey123IDWithKey"]; }; }; /** @description Error */ @@ -7561,6 +7557,31 @@ export interface operations { }; }; }; + /** @description cycles a Survey123 key (project admin) */ + "survey123-update-key": { + parameters: { + path: { + /** @example 5b6f4f37-7755-4cf9-bd02-94f1e9bc5984 */ + project_id: string; + /** @example a2e19d85-4c64-4e99-b93a-4f4f56a718cf */ + survey123_id: string; + }; + }; + responses: { + /** @description OK */ + 200: { + content: { + "application/json": components["schemas"]["Survey123IDWithKey"]; + }; + }; + /** @description Error */ + default: { + content: { + "application/problem+json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; /** @description gets a preview payload for a survey123 */ "survey123-get-preview": { parameters: { @@ -8189,7 +8210,6 @@ export interface operations { "measurement-list-for-timeseries": { parameters: { query?: { - expertimental?: boolean; threshold?: number; time_window?: string; after?: string;