wip: start manually decoding only wanted fish health fields
Also do some refactoring, and start on moving barentswatch api behind auth. The manual fish health data decoding needs some work, and needs a mapping of what is interesting. The selection is also kinda wonky right now. The selection and deselection might not be what we want.
This commit is contained in:
79
docs/fiskeridir-locality-borders-example-payload.json
Normal file
79
docs/fiskeridir-locality-borders-example-payload.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"id": 6551,
|
||||||
|
"siteVersionId": 2274,
|
||||||
|
"siteNr": 12298,
|
||||||
|
"name": "RØYSA",
|
||||||
|
"type": {
|
||||||
|
"value": "UNSPECIFIED"
|
||||||
|
},
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"id": 13885,
|
||||||
|
"index": 0,
|
||||||
|
"latitude": 62.549783,
|
||||||
|
"longitude": 6.1385
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13886,
|
||||||
|
"index": 1,
|
||||||
|
"latitude": 62.55055,
|
||||||
|
"longitude": 6.141133
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13887,
|
||||||
|
"index": 2,
|
||||||
|
"latitude": 62.547467,
|
||||||
|
"longitude": 6.145183
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13888,
|
||||||
|
"index": 3,
|
||||||
|
"latitude": 62.546717,
|
||||||
|
"longitude": 6.142533
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13889,
|
||||||
|
"index": 4,
|
||||||
|
"latitude": 62.549783,
|
||||||
|
"longitude": 6.1385
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13890,
|
||||||
|
"index": 5,
|
||||||
|
"latitude": 62.54805,
|
||||||
|
"longitude": 6.1391
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13891,
|
||||||
|
"index": 6,
|
||||||
|
"latitude": 62.5481,
|
||||||
|
"longitude": 6.139267
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13892,
|
||||||
|
"index": 7,
|
||||||
|
"latitude": 62.5479,
|
||||||
|
"longitude": 6.139533
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13893,
|
||||||
|
"index": 8,
|
||||||
|
"latitude": 62.54785,
|
||||||
|
"longitude": 6.139333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13894,
|
||||||
|
"index": 9,
|
||||||
|
"latitude": 62.54805,
|
||||||
|
"longitude": 6.1391
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13895,
|
||||||
|
"index": 10,
|
||||||
|
"latitude": 62.549783,
|
||||||
|
"longitude": 6.1385
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaM2": 64435.13037109375
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ open Remoting
|
|||||||
|
|
||||||
open Archmeister.Types
|
open Archmeister.Types
|
||||||
open Atlantis.Types
|
open Atlantis.Types
|
||||||
|
open Atlantis.Shared
|
||||||
open Maps
|
open Maps
|
||||||
|
|
||||||
importSideEffects "./public/style.scss"
|
importSideEffects "./public/style.scss"
|
||||||
|
|||||||
198
src/Client/BarentsWatch.fs
Normal file
198
src/Client/BarentsWatch.fs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
module BarentsWatch
|
||||||
|
|
||||||
|
open System
|
||||||
|
|
||||||
|
open Browser
|
||||||
|
open Fable.Core
|
||||||
|
open Thoth.Json
|
||||||
|
open Thoth.Fetch
|
||||||
|
|
||||||
|
open Atlantis.Shared
|
||||||
|
|
||||||
|
|
||||||
|
type Escape = {
|
||||||
|
Date : DateTime
|
||||||
|
Comment: string
|
||||||
|
Species: string
|
||||||
|
Count: int
|
||||||
|
CountType: string
|
||||||
|
CountCategory: string
|
||||||
|
Size: int
|
||||||
|
LocalityNo: int
|
||||||
|
LocalityName: string
|
||||||
|
Year: int
|
||||||
|
Week: int
|
||||||
|
ObjectId: int
|
||||||
|
ParentId: int
|
||||||
|
LocalityCapacity: int
|
||||||
|
WaterEnvironmentCode: string
|
||||||
|
WaterEnvironment: string
|
||||||
|
Status: string
|
||||||
|
StatusLocality: string
|
||||||
|
CreatedDate: DateTime
|
||||||
|
LastEditedDate: DateTime
|
||||||
|
GlobalId: string
|
||||||
|
CleanerFish: bool
|
||||||
|
RecaptureStarted: bool
|
||||||
|
RecaptureDescription: string
|
||||||
|
RecapturesCompleted: int
|
||||||
|
CountEstimated: string
|
||||||
|
MunicipalityNumber: int
|
||||||
|
CountyCode: int
|
||||||
|
County: string
|
||||||
|
Description: string
|
||||||
|
SizeEstimated: int
|
||||||
|
}
|
||||||
|
|
||||||
|
type IlaPd = {
|
||||||
|
Id : int
|
||||||
|
LocalityNo : int
|
||||||
|
Disease : string
|
||||||
|
Ruling : string
|
||||||
|
FromDate : DateTime
|
||||||
|
ToDate : DateTime option
|
||||||
|
}
|
||||||
|
with
|
||||||
|
static member decoder =
|
||||||
|
Decode.oneOf [
|
||||||
|
Decode.Auto.generateDecoder(
|
||||||
|
caseStrategy = CaseStrategy.CamelCase
|
||||||
|
)
|
||||||
|
Decode.nil [||]
|
||||||
|
]
|
||||||
|
|
||||||
|
type LiceCountPreviousWeek = {
|
||||||
|
Year: int
|
||||||
|
Week: int
|
||||||
|
HasReportedLice: bool
|
||||||
|
AvgAdultFemaleLice: float option
|
||||||
|
AvgMobileLice: float option
|
||||||
|
AvgStationaryLice: float option
|
||||||
|
}
|
||||||
|
with
|
||||||
|
static member decoder =
|
||||||
|
Decode.object (fun get -> {
|
||||||
|
Year = get.Required.Field "year" Decode.int
|
||||||
|
Week = get.Required.Field "week" Decode.int
|
||||||
|
HasReportedLice = get.Required.Field "hasReportedLice" Decode.bool
|
||||||
|
AvgAdultFemaleLice = get.Optional.Field "avgAdultFemaleLice" (Decode.oneOf [ Decode.float; Decode.nil 0.0 ])
|
||||||
|
AvgMobileLice = get.Optional.Field "avgMobileLice" (Decode.oneOf [ Decode.float; Decode.nil 0.0 ])
|
||||||
|
AvgStationaryLice = get.Optional.Field "avgStationaryLice" (Decode.oneOf [ Decode.float; Decode.nil 0.0 ])
|
||||||
|
})
|
||||||
|
|
||||||
|
// See docs/barentswatch-fishhealth-example-payload.json
|
||||||
|
type BarentsWatchFishHealth = {
|
||||||
|
LocalityName : string
|
||||||
|
LocalityIsInAquaCultureRegister : bool
|
||||||
|
LocalityWeek : obj
|
||||||
|
AquaCultureRegister : obj
|
||||||
|
ProductionArea : obj
|
||||||
|
LiceCountPreviousWeek : obj
|
||||||
|
ExportRestrictionAreas : obj array
|
||||||
|
// PdZoneId : string
|
||||||
|
Escapes : Escape array
|
||||||
|
// PdSurveillanceZones : obj array
|
||||||
|
// PdProtectionZones : obj array
|
||||||
|
// IlaSurveillanceZones : obj array
|
||||||
|
// IlaProtectionZones : obj array
|
||||||
|
// IlaFreeAreas : obj array
|
||||||
|
IlaPd : IlaPd array
|
||||||
|
// IlaPdCase : obj array
|
||||||
|
}
|
||||||
|
with
|
||||||
|
// TODO: Finish the manual decoder
|
||||||
|
// static member decoder =
|
||||||
|
// Decode.object (fun get ->
|
||||||
|
// {
|
||||||
|
// LocalityName = get.Required.Field "localityName" (Decode.oneOf [ Decode.string; Decode.nil "" ])
|
||||||
|
// LocalityIsInAquaCultureRegister = get.Required.Field "localityIsInAquaCultureRegister" Decode.bool
|
||||||
|
// // LocalityWeek = get.Optional.Field "localityWeek" ()
|
||||||
|
// // AquaCultureRegister = get.Optional.Field "aquaCultureRegister" Decode.object
|
||||||
|
// // ProductionArea : obj
|
||||||
|
// LiceCountPreviousWeek = get.Required.Field "liceCountPreviousWeek" LiceCountPreviousWeek.decoder
|
||||||
|
// // ExportRestrictionAreas : obj array
|
||||||
|
// // PdZoneId : string
|
||||||
|
// Escapes = get.Required.Field "escapes" (Decode.Auto.generateDecoder<Escape array>())
|
||||||
|
// // PdSurveillanceZones : obj array
|
||||||
|
// // PdProtectionZones : obj array
|
||||||
|
// // IlaSurveillanceZones : obj array
|
||||||
|
// // IlaProtectionZones : obj array
|
||||||
|
// // IlaFreeAreas : obj array
|
||||||
|
// IlaPd = get.Required.Field "ilaPd" (IlaPd.decoder)
|
||||||
|
// // IlaPdCase : obj array
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
static member autoDecoder =
|
||||||
|
Decode.Auto.generateDecoder<BarentsWatchFishHealth>(
|
||||||
|
caseStrategy = CaseStrategy.CamelCase
|
||||||
|
)
|
||||||
|
|
||||||
|
let fetchFishHealth year week localityId callback =
|
||||||
|
Fetch.tryGet(
|
||||||
|
url = $"https://www.barentswatch.no/bwapi/v1/geodata/fishhealth/locality/%i{localityId}/%i{year}/%i{week}",
|
||||||
|
headers = [
|
||||||
|
// TODO: Try to fetch token again if its empty. You might have been on the site for more than 1 hour.
|
||||||
|
Fetch.Types.HttpRequestHeaders.Authorization $"""Bearer {sessionStorage["barentswatch_token"]}"""
|
||||||
|
],
|
||||||
|
// TODO: Switch to manual decoder
|
||||||
|
decoder = BarentsWatchFishHealth.autoDecoder
|
||||||
|
)
|
||||||
|
|> Promise.iter (
|
||||||
|
Result.mapError (fun err ->
|
||||||
|
match err with
|
||||||
|
| PreparingRequestFailed exn ->
|
||||||
|
console.error("BarentsWatch get preparing request failed:", exn.Message)
|
||||||
|
| DecodingFailed s ->
|
||||||
|
console.error("BarentsWatch get decoding error:", s)
|
||||||
|
| FetchFailed response ->
|
||||||
|
response.json ()
|
||||||
|
|> Promise.iter (fun errorJson ->
|
||||||
|
console.error("BarentsWatch get fetch failed:", errorJson))
|
||||||
|
| NetworkError exn ->
|
||||||
|
console.error("BarentsWatch get network error:", exn.Message)
|
||||||
|
|
||||||
|
// TODO: Create errMsg based on error
|
||||||
|
"Could not fetch fish health data from BarentsWatch"
|
||||||
|
)
|
||||||
|
>> callback
|
||||||
|
)
|
||||||
|
|
||||||
|
let fetchToken () : BarentsWatch.Token option JS.Promise =
|
||||||
|
Fetch.tryGet(
|
||||||
|
url = "/barentswatch-token",
|
||||||
|
decoder = BarentsWatch.tokenDecoder
|
||||||
|
)
|
||||||
|
|> Promise.map (fun res ->
|
||||||
|
match res with
|
||||||
|
| Error err ->
|
||||||
|
console.error("Could not fetch BarentsWatch token", err)
|
||||||
|
None
|
||||||
|
| Ok token ->
|
||||||
|
Some token
|
||||||
|
)
|
||||||
|
|
||||||
|
let refreshLoop () : unit JS.Promise =
|
||||||
|
let rec loop () =
|
||||||
|
promise {
|
||||||
|
let! tokenOpt = fetchToken()
|
||||||
|
|
||||||
|
tokenOpt
|
||||||
|
|> Option.iter (fun token ->
|
||||||
|
console.info("Got new BarentsWatch token")
|
||||||
|
sessionStorage["barentswatch_token"] <- token.AccessToken
|
||||||
|
)
|
||||||
|
|
||||||
|
let sleepTimeMs =
|
||||||
|
tokenOpt
|
||||||
|
|> Option.map (fun token ->
|
||||||
|
let ts = TimeSpan.FromSeconds token.ExpiresIn
|
||||||
|
ts.Milliseconds)
|
||||||
|
|> Option.defaultValue (
|
||||||
|
let ts = TimeSpan.FromHours 1
|
||||||
|
ts.Milliseconds
|
||||||
|
)
|
||||||
|
do! Promise.sleep sleepTimeMs
|
||||||
|
return! loop ()
|
||||||
|
}
|
||||||
|
|
||||||
|
loop ()
|
||||||
@@ -28,12 +28,14 @@
|
|||||||
<Compile Include="Model.fs"/>
|
<Compile Include="Model.fs"/>
|
||||||
<Compile Include="MessageHub.fs"/>
|
<Compile Include="MessageHub.fs"/>
|
||||||
<Compile Include="Colors.fs"/>
|
<Compile Include="Colors.fs"/>
|
||||||
<Compile Include="Layers.fs"/>
|
<Compile Include="Layers.fs" />
|
||||||
<Compile Include="Plotting.fs"/>
|
<Compile Include="Plotting.fs"/>
|
||||||
<Compile Include="ContourPlots.fs"/>
|
<Compile Include="ContourPlots.fs"/>
|
||||||
<Compile Include="PropertyPlots.fs"/>
|
<Compile Include="PropertyPlots.fs"/>
|
||||||
<Compile Include="Fable.VisJS.fs"/>
|
<Compile Include="Fable.VisJS.fs"/>
|
||||||
<Compile Include="Timeline.fs"/>
|
<Compile Include="Timeline.fs"/>
|
||||||
|
<Compile Include="Fiskeridir.fs" />
|
||||||
|
<Compile Include="BarentsWatch.fs" />
|
||||||
<Compile Include="Drifters.fs"/>
|
<Compile Include="Drifters.fs"/>
|
||||||
<Compile Include="Probing.fs"/>
|
<Compile Include="Probing.fs"/>
|
||||||
<Compile Include="Stats.fs"/>
|
<Compile Include="Stats.fs"/>
|
||||||
|
|||||||
85
src/Client/Fiskeridir.fs
Normal file
85
src/Client/Fiskeridir.fs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
module Fiskeridir
|
||||||
|
|
||||||
|
open Browser
|
||||||
|
open Fable.OpenLayers
|
||||||
|
open Thoth.Fetch
|
||||||
|
open Thoth.Json
|
||||||
|
|
||||||
|
open Atlantis.Types
|
||||||
|
open Utils
|
||||||
|
|
||||||
|
|
||||||
|
/// See docs/fiskeridir-locality-borders-example-payload.json
|
||||||
|
type FiskeridirLocalityBorder = {
|
||||||
|
Id : int
|
||||||
|
SiteVersionId : int
|
||||||
|
SiteNr : int
|
||||||
|
Name : string
|
||||||
|
Type : obj
|
||||||
|
Points : Point array
|
||||||
|
AreaM2 : float
|
||||||
|
}
|
||||||
|
and Point = {
|
||||||
|
Id : int
|
||||||
|
Index : int
|
||||||
|
Latitude : float
|
||||||
|
Longitude : float
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchLocalityBorders (map: Map.Map) (localityId: int) =
|
||||||
|
Fetch.tryGet<unit, FiskeridirLocalityBorder array>(
|
||||||
|
url = $"https://api.fiskeridir.no/pub-aqua/api/v1/sites/{localityId}/borders",
|
||||||
|
headers = [
|
||||||
|
Fetch.Types.HttpRequestHeaders.Accept "application/json"
|
||||||
|
Fetch.Types.HttpRequestHeaders.AcceptCharset "UTF-8"
|
||||||
|
],
|
||||||
|
decoder =
|
||||||
|
Decode.Auto.generateDecoder<FiskeridirLocalityBorder array>(
|
||||||
|
caseStrategy = CaseStrategy.CamelCase
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|> Promise.map (fun res ->
|
||||||
|
match res with
|
||||||
|
| Error err -> console.error("Could not fetch locality borders from api.fiskeridir.no:", err)
|
||||||
|
| Ok borders ->
|
||||||
|
let points =
|
||||||
|
borders
|
||||||
|
|> Array.collect (fun border ->
|
||||||
|
border.Points)
|
||||||
|
|
||||||
|
if points.Length < 1 then
|
||||||
|
console.info("This site does not have borders")
|
||||||
|
()
|
||||||
|
else
|
||||||
|
let feature =
|
||||||
|
let coords : Coordinate array =
|
||||||
|
let coords =
|
||||||
|
points
|
||||||
|
|> Array.map (fun point ->
|
||||||
|
posToCoord (point.Longitude, point.Latitude)
|
||||||
|
|> coordToEpsg3857)
|
||||||
|
let first = Array.head coords
|
||||||
|
let last = coords[coords.Length - 1]
|
||||||
|
|
||||||
|
if first |> Coord.equals last then
|
||||||
|
coords
|
||||||
|
else
|
||||||
|
let first = coords |> Array.head |> Array.singleton
|
||||||
|
let linearRing = first |> Array.append coords
|
||||||
|
linearRing
|
||||||
|
|
||||||
|
let polygon =
|
||||||
|
Geometry.polygon [
|
||||||
|
geometry.coordinates [| coords |]
|
||||||
|
geometry.layout GeometryLayout.XY
|
||||||
|
]
|
||||||
|
Feature.feature [
|
||||||
|
feature.geometryOrProperties polygon
|
||||||
|
]
|
||||||
|
|
||||||
|
feature.setId localityId
|
||||||
|
feature.set("type", "polygon")
|
||||||
|
|
||||||
|
Layers.addFeatures map MapLayer.Aquaculture [| feature |]
|
||||||
|
|
||||||
|
())
|
||||||
@@ -104,7 +104,7 @@ let addFeatures map layerId (features: Feature array) =
|
|||||||
let layer = baseLayer :?> VectorLayer
|
let layer = baseLayer :?> VectorLayer
|
||||||
let source = layer.getSource() :?> VectorSource
|
let source = layer.getSource() :?> VectorSource
|
||||||
|
|
||||||
console.debug("Adding features to layer", layerId, ":", features)
|
console.debug("Adding features to layer", layerId.Id, ":", features)
|
||||||
source.addFeatures features
|
source.addFeatures features
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -139,6 +139,28 @@ let toggleVisibleFeatures map layerId =
|
|||||||
do layer.setVisible (not visible)
|
do layer.setVisible (not visible)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let updateVectorFeature map layerId (findFeatureFunc: VectorSource -> unit) =
|
||||||
|
map
|
||||||
|
|> updateBaseLayer layerId (fun baseLayer ->
|
||||||
|
let layer = baseLayer :?> VectorLayer
|
||||||
|
let source = layer.getSource() :?> VectorSource
|
||||||
|
|
||||||
|
findFeatureFunc source
|
||||||
|
)
|
||||||
|
|
||||||
|
let hideVectorFeature map layerId (findFeatureFunc: VectorSource -> Feature option) =
|
||||||
|
map
|
||||||
|
|> updateBaseLayer layerId (fun baseLayer ->
|
||||||
|
let layer = baseLayer :?> VectorLayer
|
||||||
|
let source = layer.getSource() :?> VectorSource
|
||||||
|
|
||||||
|
findFeatureFunc source
|
||||||
|
|> Option.iter (fun feature ->
|
||||||
|
console.debug("hideVectorFeature found feature:", feature)
|
||||||
|
feature.setStyle(Style.style [])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
let private categorizeFeatures (fs: Feature []) (p: Particles) =
|
let private categorizeFeatures (fs: Feature []) (p: Particles) =
|
||||||
fs
|
fs
|
||||||
|> Array.iteri (fun n f ->
|
|> Array.iteri (fun n f ->
|
||||||
@@ -760,5 +782,4 @@ let activeLayerSelector (dispatch: Msg -> unit) (model: Model) =
|
|||||||
{layers}
|
{layers}
|
||||||
</sp-radio-group>
|
</sp-radio-group>
|
||||||
</sp-accordion>
|
</sp-accordion>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ open Model
|
|||||||
open Notifier
|
open Notifier
|
||||||
open Remoting
|
open Remoting
|
||||||
open Sorcerer.Types
|
open Sorcerer.Types
|
||||||
open Thoth.Fetch
|
|
||||||
open Thoth.Json
|
open Thoth.Json
|
||||||
open Utils
|
open Utils
|
||||||
|
|
||||||
@@ -282,19 +281,33 @@ let addAquacultureHoverInteraction map dispatch : unit =
|
|||||||
let elem = popup.getElement()
|
let elem = popup.getElement()
|
||||||
let desc : Types.HTMLDivElement = elem.querySelector("div") :?> Types.HTMLDivElement
|
let desc : Types.HTMLDivElement = elem.querySelector("div") :?> Types.HTMLDivElement
|
||||||
|
|
||||||
|
// TODO: Show the same hover on polygon hover? Then we must save some information
|
||||||
if ev.selected.Length = 0 then
|
if ev.selected.Length = 0 then
|
||||||
popup.setPosition(None)
|
popup.setPosition(None)
|
||||||
else
|
else
|
||||||
let feature = ev.selected[0]
|
let feature = ev.selected[0]
|
||||||
let geom = feature.getGeometry()
|
if feature.get("type") = "polygon" then
|
||||||
let coord = geom.getExtent() |> Extent.extent.getCenter
|
()
|
||||||
let species =
|
else
|
||||||
feature.get("arter") :?> string
|
let geom = feature.getGeometry()
|
||||||
|> fun s -> if s.Length > 80 then $"{s.Substring(0, 80)}..." else s
|
let coord = geom.getExtent() |> Extent.extent.getCenter
|
||||||
elem?heading <- feature.get("lokalitet")
|
let species =
|
||||||
elem?subheading <- species
|
html $"""
|
||||||
desc.innerText <- feature.get("innehaver") :?> string
|
<span
|
||||||
popup.setPosition(Some coord)
|
style="
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 80ch;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{feature.get("arter") :?> string}
|
||||||
|
</span>"""
|
||||||
|
elem?heading <- feature.get("lokalitet")
|
||||||
|
elem?subheading <- species
|
||||||
|
desc.innerText <- feature.get("innehaver") :?> string
|
||||||
|
popup.setPosition(Some coord)
|
||||||
)
|
)
|
||||||
|
|
||||||
map.addInteraction select
|
map.addInteraction select
|
||||||
@@ -305,10 +318,12 @@ let addAquacultureClickInteraction map dispatch : unit =
|
|||||||
|
|
||||||
aquacultureLayerOpt
|
aquacultureLayerOpt
|
||||||
|> Option.iter (fun (idx, aquacultureLayer)->
|
|> Option.iter (fun (idx, aquacultureLayer)->
|
||||||
|
// TODO: This should maybe be a map click event with forEveryFeatureAt thingy instead of an actual select interaction
|
||||||
let select =
|
let select =
|
||||||
Interaction.select [
|
Interaction.select [
|
||||||
select.layers [ aquacultureLayer :?> VectorLayer ]
|
select.layers [ aquacultureLayer :?> VectorLayer ]
|
||||||
select.condition Fable.OpenLayers.Event.condition.click
|
select.condition Fable.OpenLayers.Event.condition.click
|
||||||
|
select.style (Style.style [])
|
||||||
]
|
]
|
||||||
|
|
||||||
select.on (
|
select.on (
|
||||||
@@ -317,11 +332,31 @@ let addAquacultureClickInteraction map dispatch : unit =
|
|||||||
console.debug("Clicked on aquaculture", ev.selected)
|
console.debug("Clicked on aquaculture", ev.selected)
|
||||||
if ev.selected.Length = 1 then
|
if ev.selected.Length = 1 then
|
||||||
let feature = ev.selected[0]
|
let feature = ev.selected[0]
|
||||||
let localityNumber = feature.get("loknr") |> unbox int |> Some
|
if feature.get("type") = "polygon" then
|
||||||
|
feature.getId ()
|
||||||
|
|> Some
|
||||||
|
|> SetAquaculture
|
||||||
|
|> dispatch
|
||||||
|
else
|
||||||
|
let localityNumber = feature.get("loknr") |> unbox<int>
|
||||||
|
// NOTE(simkir): This is used in the layer style to hide all features that are selected. But it
|
||||||
|
// might not be needed as the select interaction style is already set to hide the feature
|
||||||
|
feature.set("selected", "true")
|
||||||
|
if 10000 < localityNumber && localityNumber < 99999 then
|
||||||
|
Fiskeridir.fetchLocalityBorders map localityNumber |> Promise.start
|
||||||
|
|
||||||
localityNumber
|
// When you deselect a feature (should in theory only be 1), set selected to false and delete the
|
||||||
|> SetAquaculture
|
// location polygon
|
||||||
|> dispatch
|
ev.deselected
|
||||||
|
|> Array.iter (fun feature ->
|
||||||
|
let localityNumberOpt = feature.get("loknr") |> unbox<int option>
|
||||||
|
console.debug("Deselecting", feature.get("loknr"))
|
||||||
|
|
||||||
|
feature.set("selected", "false")
|
||||||
|
|
||||||
|
updateVectorFeature map MapLayer.Aquaculture (fun source ->
|
||||||
|
localityNumberOpt
|
||||||
|
|> Option.iter (source.getFeatureById >> source.removeFeature)))
|
||||||
)
|
)
|
||||||
|
|
||||||
map.addInteraction select
|
map.addInteraction select
|
||||||
@@ -898,23 +933,36 @@ let update cmd (model: Model) =
|
|||||||
manageFiskeri m k
|
manageFiskeri m k
|
||||||
m, Cmd.none
|
m, Cmd.none
|
||||||
| SetAquaculture locationIdOpt ->
|
| SetAquaculture locationIdOpt ->
|
||||||
{ model with selectedAquaculture = locationIdOpt }, Cmd.none
|
// NOTE: Fetch the borders
|
||||||
|
let fetchBorderCmd =
|
||||||
|
match locationIdOpt with
|
||||||
|
| Some id when 10000 < id && id < 99999 ->
|
||||||
|
Cmd.OfPromise.perform (Fiskeridir.fetchLocalityBorders model.map) id Noop
|
||||||
|
| _ ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
{ model with selectedAquaculture = locationIdOpt },
|
||||||
|
Cmd.batch [|
|
||||||
|
fetchBorderCmd
|
||||||
|
|]
|
||||||
| InitArchive m ->
|
| InitArchive m ->
|
||||||
refreshLayers model m
|
refreshLayers model m
|
||||||
let fly () =
|
let fly (infoLayer) =
|
||||||
// console.debug("Flying")
|
// console.debug("Flying")
|
||||||
let center = toEpsg3857 m.archive.focalPoint
|
let center = toEpsg3857 m.archive.focalPoint
|
||||||
flyTo m.map center m.archive.defaultZoom
|
flyTo m.map center m.archive.defaultZoom
|
||||||
|
|
||||||
|
infoLayer
|
||||||
let refreshLayerWithDelay t =
|
let refreshLayerWithDelay t =
|
||||||
Promise.sleep t
|
Promise.sleep t
|
||||||
|> Promise.map (fun () -> refreshLayers model m)
|
|> Promise.map (fun () -> refreshLayers model m)
|
||||||
m,
|
m,
|
||||||
Cmd.batch [
|
Cmd.batch [
|
||||||
// Cmd.OfPromise.perform getWireframe m.archive.id SetDefaultWireframe
|
// Cmd.OfPromise.perform getWireframe m.archive.id SetDefaultWireframe
|
||||||
Cmd.ofMsg (SetWireframe m.grid)
|
Cmd.ofMsg (SetWireframe m.grid)
|
||||||
Cmd.OfPromise.perform Drifters.fetchArchiveDrifters m SetDrifters
|
Cmd.OfPromise.perform Drifters.fetchArchiveDrifters m SetDrifters
|
||||||
Cmd.OfFunc.perform fly () Noop
|
Cmd.OfFunc.perform fly InfoLayer.Lokaliteter AddInfoLayer
|
||||||
Cmd.OfPromise.perform refreshLayerWithDelay 50 Noop
|
Cmd.OfPromise.perform refreshLayerWithDelay 50 Noop
|
||||||
]
|
]
|
||||||
| SetProp (pt, p) ->
|
| SetProp (pt, p) ->
|
||||||
console.debug("[MapTool] SetProp on layer", pt.Id, "to type", p.PropType.ToLabel())
|
console.debug("[MapTool] SetProp on layer", pt.Id, "to type", p.PropType.ToLabel())
|
||||||
@@ -1180,60 +1228,6 @@ let aquaculturePopup =
|
|||||||
</sp-card>
|
</sp-card>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
// See docs/barentswatch-fishhealth-example-payload.json
|
|
||||||
type BarentsWatchFishHealth = {
|
|
||||||
LocalityName : string
|
|
||||||
LocalityIsInAquaCultureRegister : bool
|
|
||||||
LocalityWeek : obj
|
|
||||||
AquaCultureRegister : obj
|
|
||||||
ProductionArea : obj
|
|
||||||
LiceCountPreviousWeek : obj
|
|
||||||
ExportRestrictionAreas : obj array
|
|
||||||
PdZoneId : string
|
|
||||||
Escapes : obj array
|
|
||||||
PdSurveillanceZones : obj array
|
|
||||||
PdProtectionZones : obj array
|
|
||||||
IlaSurveillanceZones : obj array
|
|
||||||
IlaProtectionZones : obj array
|
|
||||||
IlaFreeAreas : obj array
|
|
||||||
IlaPd : obj array option
|
|
||||||
IlaPdCase : obj array
|
|
||||||
}
|
|
||||||
with
|
|
||||||
static member decoder =
|
|
||||||
Decode.Auto.generateDecoder<BarentsWatchFishHealth>(
|
|
||||||
caseStrategy = CaseStrategy.CamelCase
|
|
||||||
)
|
|
||||||
|
|
||||||
let fetchBarentsWatchFishHealth year week localityId callback =
|
|
||||||
Thoth.Fetch.Fetch.tryGet(
|
|
||||||
url = $"https://www.barentswatch.no/bwapi/v1/geodata/fishhealth/locality/%i{localityId}/%i{year}/%i{week}",
|
|
||||||
headers = [
|
|
||||||
// TODO: Get token from Atlantis
|
|
||||||
Fetch.Types.HttpRequestHeaders.Authorization $"""Bearer {sessionStorage["barentswatch_token"]}"""
|
|
||||||
],
|
|
||||||
decoder = BarentsWatchFishHealth.decoder
|
|
||||||
)
|
|
||||||
|> Promise.iter (
|
|
||||||
Result.mapError (fun err ->
|
|
||||||
match err with
|
|
||||||
| PreparingRequestFailed exn ->
|
|
||||||
console.error("BarentsWatch get preparing request failed:", exn.Message)
|
|
||||||
| DecodingFailed s ->
|
|
||||||
console.error("BarentsWatch get decoding error:", s)
|
|
||||||
| FetchFailed response ->
|
|
||||||
response.json ()
|
|
||||||
|> Promise.iter (fun errorJson ->
|
|
||||||
console.error("BarentsWatch get fetch failed:", errorJson))
|
|
||||||
| NetworkError exn ->
|
|
||||||
console.error("BarentsWatch get network error:", exn.Message)
|
|
||||||
|
|
||||||
// TODO: Create errMsg based on error
|
|
||||||
"Could not fetch fish health data from BarentsWatch"
|
|
||||||
)
|
|
||||||
>> callback
|
|
||||||
)
|
|
||||||
|
|
||||||
[<HookComponent>]
|
[<HookComponent>]
|
||||||
let aquacultureModal (currentTime: DateTime) (aquacultureId: int option) onClose =
|
let aquacultureModal (currentTime: DateTime) (aquacultureId: int option) onClose =
|
||||||
let fishHealthJson, setFishHealth = Hook.useState<string> ""
|
let fishHealthJson, setFishHealth = Hook.useState<string> ""
|
||||||
@@ -1245,7 +1239,7 @@ let aquacultureModal (currentTime: DateTime) (aquacultureId: int option) onClose
|
|||||||
newIdOpt
|
newIdOpt
|
||||||
|> Option.iter (fun newId ->
|
|> Option.iter (fun newId ->
|
||||||
console.debug("Looking up location number", newId, "in week", week)
|
console.debug("Looking up location number", newId, "in week", week)
|
||||||
fetchBarentsWatchFishHealth currentTime.Year week newId (fun fishHealthRes ->
|
BarentsWatch.fetchFishHealth currentTime.Year week newId (fun fishHealthRes ->
|
||||||
match fishHealthRes with
|
match fishHealthRes with
|
||||||
| Error errMsg ->
|
| Error errMsg ->
|
||||||
console.error("BarentsWatch error:", errMsg)
|
console.error("BarentsWatch error:", errMsg)
|
||||||
@@ -1464,7 +1458,6 @@ let initModel () =
|
|||||||
Cmd.OfAsync.perform getIdentity () SetIdentity
|
Cmd.OfAsync.perform getIdentity () SetIdentity
|
||||||
Cmd.OfAsync.perform archive () SetArchive
|
Cmd.OfAsync.perform archive () SetArchive
|
||||||
Cmd.OfAsync.perform atmoApi.Wind.GetBarbSigns () SetArrows
|
Cmd.OfAsync.perform atmoApi.Wind.GetBarbSigns () SetArrows
|
||||||
Cmd.ofMsg (AddInfoLayer InfoLayer.Lokaliteter)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
let plotPage dispatch model =
|
let plotPage dispatch model =
|
||||||
|
|||||||
@@ -151,19 +151,23 @@ let urlFunc = // must return a lambda!
|
|||||||
// </PropertyIsEqualTo>
|
// </PropertyIsEqualTo>
|
||||||
// </And>
|
// </And>
|
||||||
// </Filter>
|
// </Filter>
|
||||||
|
// <fes:Filter>
|
||||||
|
// <fes:And>
|
||||||
|
// <fes:PropertyIsNotEqualTo>
|
||||||
|
// <fes:ValueReference>FiskeridirWFS:Akvakultur_-_Lokaliteter/FiskeridirWFS:status</fes:ValueReference>
|
||||||
|
// <fes:Literal>RETIRED</fes:Literal>
|
||||||
|
// </fes:PropertyIsNotEqualTo>
|
||||||
|
// <fes:BBOX>
|
||||||
|
// <fes:ValueReference>FiskeridirWFS:Akvakultur_-_Lokaliteter/FiskeridirWFS:shape</fes:ValueReference>
|
||||||
|
// <gml:Box srsName='EPSG:3857'>
|
||||||
|
// <gml:coordinates>{extent}</gml:coordinates>
|
||||||
|
// </gml:Box>
|
||||||
|
// </fes:BBOX>
|
||||||
|
// </fes:And>
|
||||||
|
// </fes:Filter>
|
||||||
// NOTE: THE XML IS CASE SENSITIVE!
|
// NOTE: THE XML IS CASE SENSITIVE!
|
||||||
let filter =
|
let filter =
|
||||||
$"""
|
$"""<fes:Filter><fes:And><fes:PropertyIsNotEqualTo><fes:ValueReference>FiskeridirWFS:Akvakultur_-_Lokaliteter/FiskeridirWFS:status</fes:ValueReference><fes:Literal>RETIRED</fes:Literal></fes:PropertyIsNotEqualTo><fes:BBOX><fes:ValueReference>FiskeridirWFS:Akvakultur_-_Lokaliteter/FiskeridirWFS:shape</fes:ValueReference><gml:Box srsName='EPSG:3857'><gml:coordinates>{extent}</gml:coordinates></gml:Box></fes:BBOX></fes:And></fes:Filter>"""
|
||||||
<fes:Filter>
|
|
||||||
<fes:And>
|
|
||||||
<fes:PropertyIsNotEqualTo>
|
|
||||||
<fes:ValueReference>FiskeridirWFS:Akvakultur_-_Lokaliteter/FiskeridirWFS:status</fes:ValueReference>
|
|
||||||
<fes:Literal>RETIRED</fes:Literal>
|
|
||||||
</fes:PropertyIsNotEqualTo>
|
|
||||||
<fes:BBOX><fes:ValueReference>FiskeridirWFS:Akvakultur_-_Lokaliteter/FiskeridirWFS:shape</fes:ValueReference><gml:Box srsName='EPSG:3857'><gml:coordinates>{extent}</gml:coordinates></gml:Box></fes:BBOX>
|
|
||||||
</fes:And>
|
|
||||||
</fes:Filter>
|
|
||||||
"""
|
|
||||||
|
|
||||||
Lokaliteter.wfsUrl()
|
Lokaliteter.wfsUrl()
|
||||||
+ "?SERVICE=WFS"
|
+ "?SERVICE=WFS"
|
||||||
@@ -209,12 +213,30 @@ let fiskeri (topic: InfoLayer) : Ol.Layer.Layer =
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
let polygonStyle =
|
||||||
|
Style.style [
|
||||||
|
style.stroke (
|
||||||
|
Style.stroke [
|
||||||
|
stroke.color "white"
|
||||||
|
stroke.width 1
|
||||||
|
]
|
||||||
|
)
|
||||||
|
style.fill (
|
||||||
|
Style.fill [
|
||||||
|
fill.color "rgba(0.5, 0.5, 0.5, 0.1)"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
// TODO: Try webglPoints again
|
// TODO: Try webglPoints again
|
||||||
Layer.vectorLayer [
|
Layer.vectorLayer [
|
||||||
layer.className (string MapLayer.Aquaculture)
|
layer.className (string MapLayer.Aquaculture)
|
||||||
layer.source vectorSource
|
layer.source vectorSource
|
||||||
layer.style defaultStyle
|
layer.style (fun feature ->
|
||||||
|
if feature.get("type") = "polygon" then
|
||||||
|
polygonStyle
|
||||||
|
else
|
||||||
|
defaultStyle)
|
||||||
layer.zIndex 12
|
layer.zIndex 12
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ open Fable.Remoting.Server
|
|||||||
open Giraffe
|
open Giraffe
|
||||||
open Microsoft.AspNetCore.Authentication
|
open Microsoft.AspNetCore.Authentication
|
||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
open Saturn
|
|
||||||
open Serilog
|
open Serilog
|
||||||
open Server
|
open Server
|
||||||
open StackExchange.Redis
|
|
||||||
open Thoth.Json.Net
|
open Thoth.Json.Net
|
||||||
|
open StackExchange.Redis
|
||||||
|
|
||||||
open Archmeister.Actors
|
open Archmeister.Actors
|
||||||
open Atlantis.Shared
|
open Atlantis.Shared
|
||||||
@@ -29,33 +28,6 @@ open Petimeter.Interfaces.Actors
|
|||||||
open Settings
|
open Settings
|
||||||
|
|
||||||
|
|
||||||
let fetchBarentsWatchToken clientId secret =
|
|
||||||
async {
|
|
||||||
let! tokenStr =
|
|
||||||
Http.AsyncRequestString(
|
|
||||||
// NOTE: Guessing this wont change much :)
|
|
||||||
url = "https://id.barentswatch.no/connect/token",
|
|
||||||
httpMethod = "POST",
|
|
||||||
headers = [
|
|
||||||
"Content-Type", "application/x-www-form-urlencoded"
|
|
||||||
],
|
|
||||||
body =
|
|
||||||
HttpRequestBody.TextRequest (
|
|
||||||
"scope=api"
|
|
||||||
+ "&grant_type=client_credentials"
|
|
||||||
+ $"&client_id=%s{clientId}"
|
|
||||||
+ $"&client_secret=%s{secret}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
let jsonRes =
|
|
||||||
Decode.Auto.fromString<BarentsWatchToken>(
|
|
||||||
json = tokenStr,
|
|
||||||
caseStrategy = CaseStrategy.SnakeCase
|
|
||||||
)
|
|
||||||
|
|
||||||
return jsonRes
|
|
||||||
}
|
|
||||||
|
|
||||||
let anonymous = "unknown"
|
let anonymous = "unknown"
|
||||||
|
|
||||||
let getUserName (ctx: HttpContext) =
|
let getUserName (ctx: HttpContext) =
|
||||||
@@ -97,10 +69,6 @@ module Handlers =
|
|||||||
GetFileService = fun () -> appsettings.sorcerer |> async.Return
|
GetFileService = fun () -> appsettings.sorcerer |> async.Return
|
||||||
}
|
}
|
||||||
|
|
||||||
let barentsWatchDecoder (str: string) : Result<BarentsWatchToken, string> =
|
|
||||||
Decode.Auto.fromString<BarentsWatchToken>(json = str, caseStrategy = CaseStrategy.SnakeCase)
|
|
||||||
let barentsWatchEncoder (token: BarentsWatchToken) : string =
|
|
||||||
Encode.Auto.toString(value = token, caseStrategy = CaseStrategy.SnakeCase)
|
|
||||||
let refreshDecoder =
|
let refreshDecoder =
|
||||||
Decode.fromString (Decode.Auto.generateDecoder<Refresh> ())
|
Decode.fromString (Decode.Auto.generateDecoder<Refresh> ())
|
||||||
|
|
||||||
@@ -160,54 +128,8 @@ module Handlers =
|
|||||||
Log.Information $"token refresh failed: {e}"
|
Log.Information $"token refresh failed: {e}"
|
||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
BarentsWatchToken =
|
// TODO: Remove from here as its not behind authentication
|
||||||
fun () ->
|
BarentsWatchToken = BarentsWatch.getToken
|
||||||
let key = "barentswatch_token"
|
|
||||||
let redis = ConnectionMultiplexer.Connect(appsettings.sso.redis)
|
|
||||||
let db = redis.GetDatabase()
|
|
||||||
async {
|
|
||||||
let value =
|
|
||||||
try
|
|
||||||
db.StringGet(key)
|
|
||||||
with e ->
|
|
||||||
Log.Error("Exception while fetching value from redis: {Message}", e.Message)
|
|
||||||
RedisValue()
|
|
||||||
if value.HasValue then
|
|
||||||
let tokenStr = string value
|
|
||||||
Log.Debug("Value is {Value}", tokenStr)
|
|
||||||
let token =
|
|
||||||
tokenStr
|
|
||||||
|> barentsWatchDecoder
|
|
||||||
|> Result.toOption
|
|
||||||
Log.Debug("Token is {@Token}", token)
|
|
||||||
return token
|
|
||||||
else
|
|
||||||
let! tokenRes =
|
|
||||||
barentsWatchClientID
|
|
||||||
|> Option.bind (fun id ->
|
|
||||||
barentsWatchSecret
|
|
||||||
|> Option.map (fun secret ->
|
|
||||||
fetchBarentsWatchToken id secret))
|
|
||||||
|> Option.defaultValue (
|
|
||||||
Error "Barentswatch secret or client id missing"
|
|
||||||
|> async.Return
|
|
||||||
)
|
|
||||||
|
|
||||||
match tokenRes with
|
|
||||||
| Error err ->
|
|
||||||
Log.Error(err)
|
|
||||||
return None
|
|
||||||
| Ok token ->
|
|
||||||
let tokenStr = barentsWatchEncoder token
|
|
||||||
let expire = TimeSpan.FromSeconds token.ExpiresIn
|
|
||||||
if db.StringSet(key = key, value = tokenStr, expiry = expire) then
|
|
||||||
Log.Debug("Updated barentsWatch token")
|
|
||||||
else
|
|
||||||
Log.Error("Error pushing barentsWatch token to Redis")
|
|
||||||
|
|
||||||
return Some token
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let private jobStatsToString status =
|
let private jobStatsToString status =
|
||||||
@@ -418,4 +340,4 @@ module Endpoints =
|
|||||||
Remoting.createApi ()
|
Remoting.createApi ()
|
||||||
|> Remoting.fromContext Handlers.inboxApi
|
|> Remoting.fromContext Handlers.inboxApi
|
||||||
|> Remoting.withRouteBuilder Api.routeBuilder
|
|> Remoting.withRouteBuilder Api.routeBuilder
|
||||||
|> Remoting.buildHttpHandler
|
|> Remoting.buildHttpHandler
|
||||||
81
src/Server/BarentsWatch.fs
Normal file
81
src/Server/BarentsWatch.fs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
module BarentsWatch
|
||||||
|
|
||||||
|
open System
|
||||||
|
|
||||||
|
open FSharp.Data
|
||||||
|
open Serilog
|
||||||
|
open StackExchange.Redis
|
||||||
|
|
||||||
|
open Atlantis.Shared
|
||||||
|
open Settings
|
||||||
|
|
||||||
|
|
||||||
|
/// Gets a new token from BarentsWatch
|
||||||
|
let fetchNewToken clientId secret =
|
||||||
|
async {
|
||||||
|
let! tokenStr =
|
||||||
|
Http.AsyncRequestString(
|
||||||
|
// NOTE: Guessing this wont change much :)
|
||||||
|
url = "https://id.barentswatch.no/connect/token",
|
||||||
|
httpMethod = "POST",
|
||||||
|
headers = [
|
||||||
|
"Content-Type", "application/x-www-form-urlencoded"
|
||||||
|
],
|
||||||
|
body =
|
||||||
|
HttpRequestBody.TextRequest (
|
||||||
|
"scope=api"
|
||||||
|
+ "&grant_type=client_credentials"
|
||||||
|
+ $"&client_id=%s{clientId}"
|
||||||
|
+ $"&client_secret=%s{secret}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return BarentsWatch.tokenDecode tokenStr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to see if an existing token exists in Redis, and if not, fetch a new one from BarentsWatch
|
||||||
|
let getToken () =
|
||||||
|
let key = "barentswatch_token"
|
||||||
|
let redis = ConnectionMultiplexer.Connect(appsettings.sso.redis)
|
||||||
|
let db = redis.GetDatabase()
|
||||||
|
async {
|
||||||
|
let value =
|
||||||
|
try
|
||||||
|
db.StringGet(key)
|
||||||
|
with e ->
|
||||||
|
Log.Error("Exception while fetching value from redis: {Message}", e.Message)
|
||||||
|
RedisValue()
|
||||||
|
if value.HasValue then
|
||||||
|
let tokenStr = string value
|
||||||
|
Log.Debug("Value is {Value}", tokenStr)
|
||||||
|
let token =
|
||||||
|
BarentsWatch.tokenDecode tokenStr
|
||||||
|
|> Result.toOption
|
||||||
|
Log.Debug("Token is {@Token}", token)
|
||||||
|
return token
|
||||||
|
else
|
||||||
|
let! tokenRes =
|
||||||
|
barentsWatchClientID
|
||||||
|
|> Option.bind (fun id ->
|
||||||
|
barentsWatchSecret
|
||||||
|
|> Option.map (fun secret ->
|
||||||
|
fetchNewToken id secret))
|
||||||
|
|> Option.defaultValue (
|
||||||
|
Error "Barentswatch secret or client id missing"
|
||||||
|
|> async.Return
|
||||||
|
)
|
||||||
|
|
||||||
|
match tokenRes with
|
||||||
|
| Error err ->
|
||||||
|
Log.Error(err)
|
||||||
|
return None
|
||||||
|
| Ok token ->
|
||||||
|
let tokenStr = BarentsWatch.tokenEncode token
|
||||||
|
let expire = TimeSpan.FromSeconds token.ExpiresIn
|
||||||
|
if db.StringSet(key = key, value = tokenStr, expiry = expire) then
|
||||||
|
Log.Debug("Updated barentsWatch token")
|
||||||
|
else
|
||||||
|
Log.Error("Error pushing barentsWatch token to Redis")
|
||||||
|
|
||||||
|
return Some token
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ module Main
|
|||||||
open System
|
open System
|
||||||
open System.Text.Json
|
open System.Text.Json
|
||||||
open System.Text.Json.Serialization
|
open System.Text.Json.Serialization
|
||||||
|
|
||||||
open Argu
|
open Argu
|
||||||
open Dapr.Actors
|
open Dapr.Actors
|
||||||
|
open FSharp.Data
|
||||||
open Fable.SignalR
|
open Fable.SignalR
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Microsoft.AspNetCore.Authentication
|
open Microsoft.AspNetCore.Authentication
|
||||||
@@ -17,11 +19,11 @@ open Microsoft.Extensions.DependencyInjection
|
|||||||
open Microsoft.Extensions.Logging
|
open Microsoft.Extensions.Logging
|
||||||
open Prometheus
|
open Prometheus
|
||||||
open Saturn
|
open Saturn
|
||||||
|
open Saturn.Dapr
|
||||||
open Serilog
|
open Serilog
|
||||||
open Serilog.Events
|
open Serilog.Events
|
||||||
|
|
||||||
open Server
|
open Server
|
||||||
open Saturn.Dapr
|
|
||||||
open Atlantis.Shared
|
open Atlantis.Shared
|
||||||
open Auth
|
open Auth
|
||||||
open Settings
|
open Settings
|
||||||
@@ -112,6 +114,17 @@ let getAccessToken (next: HttpFunc) (ctx: HttpContext) =
|
|||||||
return! json t next ctx
|
return! json t next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let getBarentsWatchToken (next: HttpFunc) (ctx: HttpContext) =
|
||||||
|
task {
|
||||||
|
let! tokenOpt = BarentsWatch.getToken () |> Async.StartAsTask
|
||||||
|
match tokenOpt with
|
||||||
|
| None ->
|
||||||
|
ctx.SetStatusCode 500
|
||||||
|
return! text "Could not get BarentsWatch token" next ctx
|
||||||
|
| Some token ->
|
||||||
|
return! json token next ctx
|
||||||
|
}
|
||||||
|
|
||||||
let getClaims (next: HttpFunc) (ctx: HttpContext) =
|
let getClaims (next: HttpFunc) (ctx: HttpContext) =
|
||||||
task {
|
task {
|
||||||
let c =
|
let c =
|
||||||
@@ -148,6 +161,9 @@ let webApp =
|
|||||||
>=> choose [
|
>=> choose [
|
||||||
route "/claims" >=> getClaims
|
route "/claims" >=> getClaims
|
||||||
route "/token" >=> getAccessToken
|
route "/token" >=> getAccessToken
|
||||||
|
// TODO: Start using this in front-end, and remove from authEndpoints
|
||||||
|
// TODO: Implement as Remoting?
|
||||||
|
route "/barentswatch-token" >=> getBarentsWatchToken
|
||||||
Api.Endpoints.servicesEndpoints
|
Api.Endpoints.servicesEndpoints
|
||||||
Api.Endpoints.driftersEndpoints
|
Api.Endpoints.driftersEndpoints
|
||||||
Api.Endpoints.inboxEndpoints
|
Api.Endpoints.inboxEndpoints
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<Compile Include="Saturn.Dapr.fs"/>
|
<Compile Include="Saturn.Dapr.fs"/>
|
||||||
<Compile Include="MessageHub.fs"/>
|
<Compile Include="MessageHub.fs"/>
|
||||||
<Compile Include="Auth.fs"/>
|
<Compile Include="Auth.fs"/>
|
||||||
|
<Compile Include="BarentsWatch.fs" />
|
||||||
<Compile Include="Api.fs"/>
|
<Compile Include="Api.fs"/>
|
||||||
<Compile Include="PubSub.fs"/>
|
<Compile Include="PubSub.fs"/>
|
||||||
<Compile Include="Server.fs"/>
|
<Compile Include="Server.fs"/>
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ open Drifters.ApiTypes
|
|||||||
open Hipster.Interfaces.Job
|
open Hipster.Interfaces.Job
|
||||||
open Hipster.Interfaces.Actors
|
open Hipster.Interfaces.Actors
|
||||||
open Petimeter.Interfaces.Inbox
|
open Petimeter.Interfaces.Inbox
|
||||||
|
#if FABLE_COMPILER
|
||||||
|
open Thoth.Json
|
||||||
|
#else
|
||||||
|
open Thoth.Json.Net
|
||||||
|
#endif
|
||||||
|
|
||||||
type UserCredentials = { username: string; password: string }
|
type UserCredentials = { username: string; password: string }
|
||||||
|
|
||||||
@@ -18,13 +23,23 @@ type OidToken =
|
|||||||
scope: string
|
scope: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BarentsWatchToken = {
|
module BarentsWatch =
|
||||||
AccessToken: string
|
type Token = {
|
||||||
/// In seconds. Should be 3600s as per BarentsWatch docs
|
AccessToken: string
|
||||||
ExpiresIn: int
|
/// In seconds. Should be 3600s as per BarentsWatch docs
|
||||||
TokenType: string
|
ExpiresIn: int
|
||||||
Scope: string
|
TokenType: string
|
||||||
}
|
Scope: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenDecode (str: string) : Result<Token, string> =
|
||||||
|
Decode.Auto.fromString<Token>(json = str, caseStrategy = CaseStrategy.SnakeCase)
|
||||||
|
|
||||||
|
let tokenDecoder =
|
||||||
|
Decode.Auto.generateDecoder<Token>(caseStrategy = CaseStrategy.SnakeCase)
|
||||||
|
|
||||||
|
let tokenEncode (token: Token) : string =
|
||||||
|
Encode.Auto.toString(value = token, caseStrategy = CaseStrategy.SnakeCase)
|
||||||
|
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Api =
|
module Api =
|
||||||
@@ -47,7 +62,7 @@ module Api =
|
|||||||
IsAuthenticated: unit -> Async<(string * string) option>
|
IsAuthenticated: unit -> Async<(string * string) option>
|
||||||
HasRoleOf: string -> Async<bool>
|
HasRoleOf: string -> Async<bool>
|
||||||
RefreshAccessToken: string -> Async<(string * string) option>
|
RefreshAccessToken: string -> Async<(string * string) option>
|
||||||
BarentsWatchToken: unit -> Async<BarentsWatchToken option>
|
BarentsWatchToken: unit -> Async<BarentsWatch.Token option>
|
||||||
}
|
}
|
||||||
|
|
||||||
type Services =
|
type Services =
|
||||||
@@ -136,5 +151,4 @@ module Hub =
|
|||||||
| Note of Notification.Note
|
| Note of Notification.Note
|
||||||
| InboxUnread of int
|
| InboxUnread of int
|
||||||
| Success
|
| Success
|
||||||
| Failure of string
|
| Failure of string
|
||||||
|
|
||||||
Reference in New Issue
Block a user