Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
189 commits
Select commit Hold shift + click to select a range
a6a00fe
Nerf the thrall migration thread; very noisy.
tonytw1 Jun 4, 2024
0c53747
Delete SyncChecker; not needed yet
tonytw1 May 18, 2024
07dd5c7
Delete Scripts; not needed yet and difficult.
tonytw1 May 18, 2024
3708a07
Delete GoodToGoCheck; not instance specific.
tonytw1 Jun 25, 2024
c745595
Introduce an Instance model class and a way to infer it from a request.
tonytw1 Aug 29, 2024
c4a4fa3
Multi tenant vhosts - Redefine Service URLs to be a function of insta…
tonytw1 Nov 23, 2024
0b26022
Marking TODO corsFilter and securityHeadersFilter will need to be rew…
tonytw1 May 16, 2024
316ee16
Reinstate security headers using an instance specific config.
tonytw1 Aug 10, 2024
ef80422
Implement instance specific CORS filter.
tonytw1 Aug 12, 2024
a2d4364
Reinstate CSRF filter with needed the CORS filter to single bypass on…
tonytw1 Aug 12, 2024
7bdc2b5
Kahuna CSP origin.full and origin.images are believed to be redundant…
tonytw1 Nov 24, 2024
c726d5f
Resolve TODO. auth and kahuna are on the same hostname as everyother …
tonytw1 Nov 24, 2024
72bdd13
Kahuna auth uri in main.view are correct.
tonytw1 Aug 30, 2024
9ee964d
Fix share URL is double //
tonytw1 May 22, 2024
404e891
Initial instance awareness. Prepare upload records a request specific…
tonytw1 Mar 14, 2026
c508069
Message instance field moves up from ImageUpdate to all ExternalThral…
tonytw1 Aug 29, 2024
c0b1201
Media API's ElasticSearch class accepts instances inputs into current…
tonytw1 Aug 29, 2024
393f9f4
Media API's Elastic responses are instance specific.
tonytw1 Jul 23, 2026
3671738
Step back to service to service calls via the full public URLs so tha…
tonytw1 Sep 8, 2024
7c83b15
Introduce a typed Instance and pass it around implicitly.
tonytw1 Jul 23, 2026
78db0cd
Patch instance url s"" usages with toString.
tonytw1 May 19, 2024
901e90a
URIs take a full Instance rather than Request; use more implicits so …
tonytw1 Nov 10, 2024
e5e7d13
Crop paths are prefixed with instance id like originals and thumbs.
tonytw1 Aug 29, 2024
c06e10d
Collections store instance aware using Dynamo composite keys on id + …
tonytw1 May 19, 2024
0b2a2b1
Instance aware all collections end points.
tonytw1 May 19, 2024
1338589
ImageCollectionsStore is instance aware.
tonytw1 Feb 26, 2026
631bcb0
Spike; progating instance throught the RX? channels for simple Usage …
tonytw1 Jun 10, 2024
f44fe1e
DynamoDB force implicit instance.
tonytw1 Aug 7, 2026
35529f1
[dynamo] scanForId is instances aware.
tonytw1 Aug 7, 2026
542926c
[dynamo] batchGet query is instance aware.
tonytw1 Aug 7, 2026
c6439d5
[dynamo] get is instance aware.
tonytw1 Aug 7, 2026
4d8b348
InstanceAwareDynamoDB asJsObject strips instance key.
tonytw1 Aug 7, 2026
1d0521d
deleteItemV2 is instance aware.
tonytw1 Aug 7, 2026
cc53794
Move Leases to id + instance composite key; Scanomo had to be by past…
tonytw1 Nov 23, 2024
897ab15
LeaseStoreSpec is instance aware.
tonytw1 Mar 17, 2026
729bb69
Upload status table reads are instance aware.
tonytw1 Nov 10, 2024
3c74d19
Patch up soft deletes table best we can given that Scanmomo doesn't s…
tonytw1 Nov 10, 2024
d9f8ea3
Just stick instance in soft delete images; not that public facing.
tonytw1 Nov 10, 2024
3449d98
[dut] WIP pushing instance down to Usages table for instance aware qu…
tonytw1 Mar 13, 2026
be188c8
[dut] Prefix usage grouping with instance slash to make filterable by…
tonytw1 Jun 29, 2024
d22556c
UsageRecord persists an instance field so that it can be used as the …
tonytw1 Jun 29, 2024
25a720b
UsageRecord persists an instance field so that it can be used as the …
tonytw1 Aug 8, 2026
b3e0852
[dynamo] UsageTableTest instance aware tests.
tonytw1 Mar 15, 2026
6b28251
UsageRecord persists an instance field tests.
tonytw1 Aug 28, 2026
ce27185
DynamoDB needs instance Partition key like the old InstanceAwareDynam…
tonytw1 Aug 28, 2026
8b957d7
EditsStoreTest sets up instance Partition key in test table.
tonytw1 Aug 28, 2026
913da74
EditsStoreTest clean ups.
tonytw1 Aug 28, 2026
421ebf4
Begin integrating the auth provider Kinde.
tonytw1 May 21, 2024
27a85a4
Logout by clearing play session.
tonytw1 May 21, 2024
833f46a
Redirect out of auth.
tonytw1 May 22, 2024
ede5500
/logout link needs a slash in this setup.
tonytw1 May 22, 2024
781b8a6
Use seperate cookie (not Play session) so it be copied for internal c…
tonytw1 May 22, 2024
cda0dfb
Flush cookie.
tonytw1 May 22, 2024
be03be1
Logout from Kinde on logout.
tonytw1 May 22, 2024
cff6adf
Auth can run at the root of the domain to auth all instances.
tonytw1 May 22, 2024
658badf
Default exit to /
tonytw1 May 22, 2024
cef7067
auth cookie not been seen on subdomains without an explicit domain set!?
tonytw1 May 22, 2024
38ba9c9
Redirect logout back to home page.
tonytw1 May 22, 2024
8b650e9
/auth/session needs to be on the same host cause CORS.
tonytw1 May 22, 2024
bf80093
Capture loggedin user cookie for use with onBehalfOf like Panda.
tonytw1 May 22, 2024
c3fc495
/auth/session needs to be on the same host cause CORS.
tonytw1 May 22, 2024
5ddfcea
Sign Kinde logged in user cookies.
tonytw1 May 23, 2024
b6b4bd1
Capture all Kinde user details.
tonytw1 May 23, 2024
3511317
Resolved TODO; random session backed state for Kinde oauth dance.
tonytw1 May 25, 2024
94e0409
Auth decorates authed user cookie with their instances on oauth callb…
tonytw1 May 30, 2024
4ef29a4
Auth provider decorates Principal with allowed instances. Definition …
tonytw1 Jun 1, 2024
6daf33a
Use domainRoot is the as the config hook to remove hard coded griddev…
tonytw1 Jun 3, 2024
d3a355a
Inline Await and timeout on Kinde request.
tonytw1 Jun 4, 2024
84bb156
Allow InnerServicePrincipal to access all instances.
tonytw1 Jun 6, 2024
dde0de2
Setup for non instance principals instances lookup.
tonytw1 Jun 6, 2024
b6f1737
Auth calls back to landing service for users instances instead of rea…
tonytw1 Jun 6, 2024
b5f09ba
My instances endpoint for auth moves to config.
tonytw1 Jun 21, 2024
e84f0b8
Quiet logging.
tonytw1 Jun 10, 2024
b5258fc
Clean up; useless future.
tonytw1 Oct 26, 2024
0b2df6b
Introduce CreateInstanceMessage so that thrall can respond to an on d…
tonytw1 May 28, 2024
95bd165
CreateInstance is added to ExternalThrallMessage and passes through t…
tonytw1 Nov 23, 2024
87d9b11
Thrall create instance calls collections to create the root Home coll…
tonytw1 Aug 30, 2024
b97644d
Sign cropper assets URLs.
tonytw1 Aug 28, 2026
275481b
Delete crops is instance aware.
tonytw1 Dec 3, 2024
a0f770a
Thrall polls for list of current instances; setting up for collection…
tonytw1 Aug 28, 2026
3e5849b
Thrall counts total ES images foreach instance.
tonytw1 Jun 15, 2024
d479991
Thrall knows image count and total file size for instances.
tonytw1 Jun 25, 2024
7639fb6
Thrall announces instance usage onto an SQS queue for instances to pi…
tonytw1 Nov 23, 2024
150c490
Thralls call back to instances endpoint is conf.
tonytw1 Jun 16, 2024
8f1dbc9
Thrall can ensure index on start up to help rebuild Elastic.
tonytw1 Jun 22, 2024
45a73ea
Push instance all the way down to fileKeyFromId to catch all usages.
tonytw1 Nov 10, 2024
944b618
Instance specific Reaper
tonytw1 Jun 25, 2024
42ec983
Clean up; extract method.
tonytw1 Jul 12, 2024
cf3fc31
Renable image-deleted messages.
tonytw1 Sep 5, 2024
649bae6
Restore image-deleted messages and make instance specific.
tonytw1 Sep 5, 2024
8743077
Marking TODO; no image-deleted notification after hard reap?
tonytw1 Nov 10, 2024
eab1f8f
Ingest images from instance specific top level folders in the ingest …
tonytw1 Feb 28, 2026
6dcb3d5
fileKeyFromId uses implicit Instance for smaller diff.
tonytw1 Aug 9, 2025
330ced5
optimisedPngKeyFromId uses implicit Instance for smaller diff.
tonytw1 Aug 9, 2025
67bd991
Log principal for blocked request.
tonytw1 Sep 10, 2024
3987d8c
Enable API key access to ownered instances. Request owner instances b…
tonytw1 Sep 10, 2024
4ded919
Remove deprecated TODO
tonytw1 Nov 7, 2024
fe6e240
Clean up.
tonytw1 Nov 7, 2024
2466c29
Logging.
tonytw1 Dec 6, 2024
f29d21c
Instance usage message adds softDeletedCount field.
tonytw1 Sep 23, 2024
48c8003
Logging.
tonytw1 Sep 23, 2024
80faecb
Refactor; extract instance message sender.
tonytw1 Sep 25, 2024
9b88878
Instance usage JSON writes.
tonytw1 Sep 25, 2024
b04124e
Setting up to send instance message after setup has completed; SQS is…
tonytw1 Oct 4, 2024
3a73f6d
Multiplex instance related messages.
tonytw1 Sep 25, 2024
30fc4c1
Reaper query build logging.
tonytw1 Sep 26, 2024
5ce7fac
Reaper controller logging.
tonytw1 Sep 26, 2024
a243dfe
KeyStore seperates API keys by instance folder.
tonytw1 Oct 25, 2024
a1c42c3
CSRF filter still problematic.
tonytw1 Oct 27, 2024
f00c299
Marking TODO.
tonytw1 Nov 3, 2024
bd39484
Log /management/healthcheck as debug.
tonytw1 Nov 5, 2024
8c34607
Private.
tonytw1 Nov 7, 2024
83c5a72
Update TODO.
tonytw1 Nov 7, 2024
77ccb8a
Disable Guardian email parsing of CSV file in bucket usage updates.
tonytw1 Nov 15, 2024
996be40
Refactor; QuotaStore in it's own file to make it more visible.
tonytw1 Nov 15, 2024
e1de4bc
usage/quotas needs a / prefix.
tonytw1 Nov 17, 2024
055b3ee
Thrall exposes a /config end point so that reaping and hard delete co…
tonytw1 Nov 29, 2024
b9e9d96
Hard delete can delete any image which is in the soft deleted state; …
tonytw1 Nov 29, 2024
965f635
Thrall exposes a /config end point so that reaping and hard delete co…
tonytw1 Nov 29, 2024
9c8da1d
Logging.
tonytw1 Nov 29, 2024
8280dcd
Log delete folder number of files found.
tonytw1 Dec 3, 2024
6b51b5b
[usage-quotas] Log quota store fetch.
tonytw1 Dec 14, 2024
6f4bdc4
[usage-quotas] Agency quota + no-op usage.
tonytw1 Dec 14, 2024
6bb1325
Billing. Setup a new actor to capture usage events.
tonytw1 Jan 12, 2025
7d37e18
Move usage events to common lib for reuse.
tonytw1 Jan 6, 2025
aa250e0
Usage event for download original.
tonytw1 Jan 6, 2025
9dc33f7
Usage event SQS sending.
tonytw1 Jan 6, 2025
6427025
Usage event includes date.
tonytw1 Jan 7, 2025
d96c05b
Emit api key used event on api auth.
tonytw1 Jan 11, 2025
1c7f110
Thrall config end point exposes maybeUploadLimitInBytes for UI.
tonytw1 Jan 12, 2025
56d8468
maybeUploadLimitInBytes is in actual MB (1024 * 1024).
tonytw1 Jan 12, 2025
a5d31af
Remove source.secureURL from image response.
tonytw1 Jan 14, 2025
7452424
Set isFeedUpload field on uploaded and ingested images.
tonytw1 Jan 14, 2025
4212bd1
isFeedUpload moves to UploadInfo.
tonytw1 Jan 15, 2025
47e9b64
Crops sign source.file to recreate secureUrl.
tonytw1 Feb 7, 2026
fa169cb
Correct image if for ingest events.
tonytw1 Jan 19, 2025
8ab189c
Clean up; auth instance call simplied.
tonytw1 Jan 20, 2025
5ef25ca
Spike; machine auth has non user endpoint.
tonytw1 Jan 20, 2025
4f00584
Api keys have instances attribute; can be checked without an attached…
tonytw1 Jan 21, 2025
faeafd2
Log levels.
tonytw1 Feb 5, 2025
03b5818
api key usage is recorded by key name.
tonytw1 Jan 23, 2025
c4b1160
Remove persistence.identifier config option which was historically se…
tonytw1 Jan 25, 2025
1b4d51f
Debug - log updateStatus
tonytw1 Jan 25, 2025
7009df3
Debug - prepared to queue request fails.
tonytw1 Jan 25, 2025
9e3b1cd
Usage event for user auth.
tonytw1 Jan 23, 2025
457d1fc
Status.toString does not match the persisted format of the Dynamo tab…
tonytw1 Jan 25, 2025
fbbb190
Debug - problem with uploadStatusTableWithCondition
tonytw1 Jan 25, 2025
c7a4b74
Fix Prepare -> Queued conditional update by burning table and redefin…
tonytw1 Jan 25, 2025
d15155d
Ingest image presigned uploads need to be into the instance folders.
tonytw1 Jan 25, 2025
bb8bd62
Disable publishChangedSyndicationRightsForPhotoshoot
tonytw1 Jan 29, 2025
92e5e1a
Usage event for prepare upload.
tonytw1 Jan 30, 2025
ae21dda
Usage events; would like to capture user or api key inline.
tonytw1 Jan 30, 2025
83e56b3
Usage events; would like to capture user or api key inline.
tonytw1 Jan 30, 2025
80224f6
Usage events; api / user recorded for prepare upload.
tonytw1 Jan 30, 2025
f7f1163
Usage events; api / user recorded for download image.
tonytw1 Jan 30, 2025
ea89fea
Move instances to common lib.
tonytw1 Feb 1, 2025
3a6038c
Move instances to common lib.
tonytw1 Feb 1, 2025
090a810
Thrall emits image delete events.
tonytw1 Feb 2, 2025
b288671
Preview reapable query by removing the time window when selected as i…
tonytw1 Feb 3, 2025
3d9367d
ReapableEligibility checks for isFeedUpload.
tonytw1 Feb 7, 2025
94a7ef7
Logging.
tonytw1 Feb 7, 2025
abd6471
ReapableEligibility checks for isFeedUpload.
tonytw1 Feb 7, 2025
e8bb6d8
ReapableEligibility checks for isFeedUpload.
tonytw1 Feb 7, 2025
013bbbf
Log upload original mime type.
tonytw1 Feb 15, 2025
b580a66
Feed ingest file uploader is last folder in path.
tonytw1 Feb 26, 2025
6f425cb
Feed ingest file uploader is folder after feeds.
tonytw1 Feb 26, 2025
c489d23
Feed ingest file uploader is folder after feeds.
tonytw1 Feb 26, 2025
92653c4
Generalise text for Chargable usage rights description.
tonytw1 Apr 12, 2025
d8500ed
Log total reapable images.
tonytw1 Apr 14, 2025
662c3d4
private
tonytw1 Apr 19, 2025
4fb564e
Thrall kinesis appName is explicitly set rather than defaulting to th…
tonytw1 Apr 20, 2025
ef94bcb
Thrall kinesis appName is explicitly set rather than defaulting to th…
tonytw1 Apr 20, 2025
847bd3c
Log stream name for Kinesis put.
tonytw1 Apr 20, 2025
5f13005
Log initial offset.
tonytw1 Apr 20, 2025
8e0263a
Mulitple KCL streams cannot share the same checkpointing table!
tonytw1 Apr 20, 2025
82eb783
Increase polling interval for unused /notification to 10 minutes.
tonytw1 Apr 28, 2025
4e21e9c
Generalise text for Chargable usage rights description.
tonytw1 Apr 12, 2025
33725f6
Generalise owned by. Screen grab description uses generalised text.
tonytw1 Apr 13, 2025
aabaf28
Generalise owned by. Remove org-owned: prefix on image thumbnail mous…
tonytw1 Apr 12, 2025
9e093f4
Generalise owned filter names.
tonytw1 Apr 12, 2025
d05b2dc
Generalise owned by - is suggestion names.
tonytw1 Feb 5, 2026
9f4fa5f
Generalise the sensitive content explainer.
tonytw1 Mar 1, 2026
ecd00c8
Instances is more testable if injected rather than mixed in.
tonytw1 Sep 21, 2025
6ec3bd7
Setting up for instance aware migration. Push down the hard-coded dum…
tonytw1 Aug 17, 2025
f38eeef
Migration source and status have list of instances.
tonytw1 Aug 17, 2025
a91ac52
[multi-tenant] Refactor; group getMyInstances into Instances trait.
tonytw1 Nov 2, 2025
efe5cca
[multi-tenant] Do not quietly recover from instances call failures. N…
tonytw1 Nov 2, 2025
ee4f936
[multi-tenant] Download export link is instance aware
tonytw1 Feb 8, 2026
b922262
[multi-tenant] EmbedderMessage is instance aware.
tonytw1 Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions auth/app/auth/AuthConfig.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package auth

import com.gu.mediaservice.lib.config.{CommonConfig, GridConfigResources}
import com.gu.mediaservice.model.Instance

class AuthConfig(resources: GridConfigResources) extends CommonConfig(resources) {
val rootUri: String = services.authBaseUri
val mediaApiUri: String = services.apiBaseUri
val rootUri: Instance => String = services.authBaseUri
val rootInstanceUri: Instance => String = services.authBaseInstanceUri
val mediaApiUri: Instance => String = services.apiBaseUri
}
21 changes: 13 additions & 8 deletions auth/app/auth/AuthController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import com.gu.mediaservice.lib.auth.Authentication.{InnerServicePrincipal, Machi
import com.gu.mediaservice.lib.auth.Permissions.{DeleteImage, ShowPaid, UploadImages}
import com.gu.mediaservice.lib.auth.provider.AuthenticationProviders
import com.gu.mediaservice.lib.auth.{Authentication, Authorisation, Internal}
import com.gu.mediaservice.lib.config.InstanceForRequest
import com.gu.mediaservice.lib.guardian.auth.PandaAuthenticationProvider
import com.gu.mediaservice.model.Instance
import play.api.libs.json.Json
import play.api.mvc.{BaseController, ControllerComponents, Result}
import play.api.mvc.{AnyContent, BaseController, ControllerComponents, Request, Result}

import java.net.URI
import java.time.Instant
Expand All @@ -19,15 +21,15 @@ class AuthController(auth: Authentication, providers: AuthenticationProviders, v
override val controllerComponents: ControllerComponents,
authorisation: Authorisation)(implicit ec: ExecutionContext)
extends BaseController
with ArgoHelpers {
with ArgoHelpers with InstanceForRequest {

val indexResponse = {
def indexResponse()(implicit instance: Instance) = {
val indexData = Map("description" -> "This is the Auth API")
val indexLinks = List(
Link("root", config.mediaApiUri),
Link("login", config.services.loginUriTemplate),
Link("ui:logout", s"${config.rootUri}/logout"),
Link("session", s"${config.rootUri}/session")
Link("root", config.mediaApiUri(instance)),
Link("login", config.services.loginUriTemplate(instance)),
Link("ui:logout", s"${config.rootUri(instance)}/logout"),
Link("session", s"${config.rootInstanceUri(instance)}/session")
)
respond(indexData, indexLinks)
}
Expand All @@ -45,7 +47,10 @@ class AuthController(auth: Authentication, providers: AuthenticationProviders, v
}
}

def index = auth { indexResponse }
def index = auth { request =>
implicit val instance: Instance = instanceOf(request)
indexResponse()
}

def session = auth { request =>
val showPaid = authorisation.hasPermissionTo(ShowPaid)(request.user)
Expand Down
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ lazy val thrall = playProject("thrall", 9002)
"software.amazon.awssdk" % "dynamodb" % awsSdkV2Version,
"com.gu" %% "kcl-pekko-stream" % "0.1.2",
"org.testcontainers" % "testcontainers-elasticsearch" % "2.0.2" % Test,
"com.google.protobuf" % "protobuf-java" % "3.19.6"
"com.google.protobuf" % "protobuf-java" % "3.19.6",
"software.amazon.awssdk" % "sqs" % awsSdkV2Version
),
dependencyOverrides ++= Seq(
"org.apache.pekko" %% "pekko-stream" % "1.0.3"
Expand Down
2 changes: 1 addition & 1 deletion collections/app/CollectionsComponents.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class CollectionsComponents(context: Context) extends GridComponents(context, ne
val notifications = new Notifications(config)

val collections = new CollectionsController(auth, config, collectionsStore, controllerComponents)
val imageCollections = new ImageCollectionsController(auth, config, notifications, imageCollectionsStore, controllerComponents)
val imageCollections = new ImageCollectionsController(auth, notifications, imageCollectionsStore, controllerComponents)


override val router = new Routes(httpErrorHandler, collections, imageCollections, management)
Expand Down
71 changes: 39 additions & 32 deletions collections/app/controllers/CollectionsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import com.gu.mediaservice.lib.argo.model.{EmbeddedEntity, Link}
import com.gu.mediaservice.lib.auth.Authentication
import com.gu.mediaservice.lib.auth.Authentication.getIdentity
import com.gu.mediaservice.lib.collections.CollectionsManager
import com.gu.mediaservice.model.{ActionData, Collection}
import com.gu.mediaservice.lib.config.InstanceForRequest
import com.gu.mediaservice.model.{ActionData, Collection, Instance}
import lib.CollectionsConfig
import model.Node
import org.joda.time.DateTime
import play.api.libs.functional.syntax._
import play.api.libs.json._
import play.api.mvc.{BaseController, ControllerComponents}
import play.api.mvc.{BaseController, ControllerComponents, Request}
import store.{CollectionsStore, CollectionsStoreError}
import com.gu.mediaservice.lib.net.{URI => UriOps}
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
import com.gu.mediaservice.lib.net.{URI => UriOps}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
Expand All @@ -31,31 +33,31 @@ object AppIndex {
}

class CollectionsController(authenticated: Authentication, config: CollectionsConfig, store: CollectionsStore,
val controllerComponents: ControllerComponents) extends BaseController with ArgoHelpers {
val controllerComponents: ControllerComponents) extends BaseController with ArgoHelpers with InstanceForRequest {

import CollectionsManager.{getCssColour, isValidPathBit, pathToUri, uriToPath}
// Stupid name clash between Argo and Play
import com.gu.mediaservice.lib.argo.model.{Action => ArgoAction}

def uri(u: String) = URI.create(u)
val collectionUri = uri(s"${config.rootUri}/collections")
def collectionUri(p: List[String] = Nil) = {
private def uri(u: String) = URI.create(u)
private def collectionUri()(implicit instance: Instance) = uri(s"${config.rootUri(instance)}/collections")
private def collectionUri(p: List[String] = Nil)(implicit instance: Instance) = {
val path = if(p.nonEmpty) s"/${pathToUri(p)}" else ""
uri(s"${config.rootUri}/collections$path")
uri(s"${config.rootUri(instance)}/collections$path")
}

val appIndex = AppIndex("media-collections", "The one stop shop for collections")
val indexLinks = List(Link("collections", collectionUri.toString))
private val appIndex = AppIndex("media-collections", "The one stop shop for collections")
private def indexLinks()(implicit instance: Instance) = List(Link("collections", collectionUri().toString))

def getNodeAction(n: Node[Collection]): Option[Link] = Some(Link("collection", collectionUri(n.fullPath).toString))
def addChildAction(pathId: List[String] = Nil): Option[ArgoAction] = Some(ArgoAction("add-child", collectionUri(pathId), "POST"))
def addChildAction(n: Node[Collection]): Option[ArgoAction] = addChildAction(n.fullPath)
def removeNodeAction(n: Node[Collection]): Option[ArgoAction] = if (n.children.nonEmpty) None else Some(
private def getNodeAction(n: Node[Collection])(implicit instance: Instance): Option[Link] = Some(Link("collection", collectionUri(n.fullPath).toString))
private def addChildAction(pathId: List[String] = Nil)(implicit instance: Instance): Option[ArgoAction] = Some(ArgoAction("add-child", collectionUri(pathId), "POST"))
private def addChildAction(n: Node[Collection])(implicit instance: Instance): Option[ArgoAction] = addChildAction(n.fullPath)
private def removeNodeAction(n: Node[Collection])(implicit instance: Instance): Option[ArgoAction] = if (n.children.nonEmpty) None else Some(
ArgoAction("remove", collectionUri(n.fullPath), "DELETE")
)

def index = authenticated { req =>
respond(appIndex, links = indexLinks)
respond(appIndex, links = indexLinks()(instanceOf(req)))
}

def collectionNotFound(path: String) =
Expand All @@ -70,15 +72,16 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
def storeError(message: String) =
respondError(InternalServerError, "collection-store-error", message)

def getActions(n: Node[Collection]): List[ArgoAction] = {
def getActions(n: Node[Collection])(implicit instance: Instance): List[ArgoAction] = {
List(addChildAction(n), removeNodeAction(n)).flatten
}

def getLinks(n: Node[Collection]): List[Link] = {
private def getLinks(n: Node[Collection])(implicit instance: Instance): List[Link] = {
List(getNodeAction(n)).flatten
}

def correctedCollections = authenticated.async { req =>
implicit val instance: Instance = instanceOf(req)
store.getAll flatMap { collections =>
val tree = Node.fromList[Collection](
collections,
Expand All @@ -100,14 +103,15 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
}
}

def allCollections = store.getAll.map { collections =>
def allCollections()(implicit instance: Instance)= store.getAll.map { collections =>
Node.fromList[Collection](
collections,
(collection) => collection.path,
(collection) => collection.description)
}

def getCollection(collectionPathId: String) = authenticated.async {
def getCollection(collectionPathId: String) = authenticated.async { request =>
implicit val instance: Instance = instanceOf(request)
store.get(uriToPath(collectionPathId)).map {
case Some(collection) =>
val node = Node(collection.path.last, Nil, collection.path, collection.path, Some(collection))
Expand All @@ -120,7 +124,18 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
}

def getCollections = authenticated.async { req =>
allCollections.map { tree =>
implicit val instance: Instance = instanceOf(req)
implicit def asArgo: Writes[Node[Collection]] = (
(__ \ "basename").write[String] ~
(__ \ "children").lazyWrite[CollectionsEntity](Writes[CollectionsEntity]
// This is so we don't have to rewrite the Write[Seq[T]]
(seq => Json.toJson(seq))).contramap(collectionsEntity(_: List[Node[Collection]])) ~
(__ \ "fullPath").write[List[String]] ~
(__ \ "data").writeNullable[Collection] ~
(__ \ "cssColour").writeNullable[String]
)(node => (node.basename, node.children, node.fullPath, node.data, getCssColour(node.fullPath)))

allCollections().map { tree =>
respond(
Json.toJson(tree)(asArgo),
actions = List(addChildAction()).flatten
Expand All @@ -134,6 +149,7 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
def addChildToRoot = addChildTo(None)
def addChildToCollection(collectionPathId: String) = addChildTo(Some(collectionPathId))
def addChildTo(collectionPathId: Option[String]) = authenticated.async(parse.json) { req =>
implicit val instance: Instance = instanceOf(req)
(req.body \ "data").asOpt[String] map { child =>
if (isValidPathBit(child)) {
val path = collectionPathId.map(uriToPath).getOrElse(Nil) :+ child
Expand All @@ -153,8 +169,8 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
}

type MaybeTree = Option[Node[Collection]]
def hasChildren(path: List[String]): Future[Boolean] =
allCollections.map { tree =>
private def hasChildren(path: List[String])(implicit instance: Instance) =
allCollections().map { tree =>

// Traverse the tree using the path
val maybeTree = path
Expand All @@ -168,6 +184,7 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
}

def removeCollection(collectionPath: String) = authenticated.async { req =>
implicit val instance: Instance = instanceOf(req)
val path = CollectionsManager.uriToPath(UriOps.encodePlus(collectionPath))

hasChildren(path).flatMap { noRemove =>
Expand Down Expand Up @@ -195,18 +212,8 @@ class CollectionsController(authenticated: Authentication, config: CollectionsCo
)(node => (node.basename, node.children, node.fullPath, node.data))

type CollectionsEntity = Seq[EmbeddedEntity[Node[Collection]]]
implicit def asArgo: Writes[Node[Collection]] = (
(__ \ "basename").write[String] ~
(__ \ "children").lazyWrite[CollectionsEntity](Writes[CollectionsEntity]
// This is so we don't have to rewrite the Write[Seq[T]]
(seq => Json.toJson(seq))).contramap(collectionsEntity) ~
(__ \ "fullPath").write[List[String]] ~
(__ \ "data").writeNullable[Collection] ~
(__ \ "cssColour").writeNullable[String]
)(node => (node.basename, node.children, node.fullPath, node.data, getCssColour(node.fullPath)))


def collectionsEntity(nodes: List[Node[Collection]]): CollectionsEntity = {
private def collectionsEntity(nodes: List[Node[Collection]])(implicit instance: Instance): CollectionsEntity = {
nodes.map(n => EmbeddedEntity(collectionUri(n.fullPath), Some(n), links = getLinks(n), actions = getActions(n)))
}

Expand Down
17 changes: 10 additions & 7 deletions collections/app/controllers/ImageCollectionsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,28 @@ import com.gu.mediaservice.lib.auth.Authentication
import com.gu.mediaservice.lib.auth.Authentication.getIdentity
import com.gu.mediaservice.lib.aws.{NoItemFound, UpdateMessage}
import com.gu.mediaservice.lib.collections.CollectionsManager
import com.gu.mediaservice.lib.config.InstanceForRequest
import com.gu.mediaservice.lib.net.{URI => UriOps}
import com.gu.mediaservice.model.{ActionData, Collection}
import com.gu.mediaservice.model.{ActionData, Collection, Instance}
import com.gu.mediaservice.syntax.MessageSubjects
import lib.{CollectionsConfig, Notifications}
import lib.Notifications
import org.joda.time.DateTime
import play.api.libs.json.Json
import play.api.mvc.{BaseController, ControllerComponents}
import store.ImageCollectionsStore

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future


class ImageCollectionsController(authenticated: Authentication, config: CollectionsConfig, notifications: Notifications,
class ImageCollectionsController(authenticated: Authentication, notifications: Notifications,
imageCollectionsStore: ImageCollectionsStore,
override val controllerComponents: ControllerComponents)
extends BaseController with MessageSubjects with ArgoHelpers {
extends BaseController with MessageSubjects with ArgoHelpers with InstanceForRequest {

import CollectionsManager.onlyLatest

def getCollections(id: String) = authenticated.async { req =>
implicit val instance: Instance = instanceOf(req)
imageCollectionsStore.get(id).map { collections =>
respond(onlyLatest(collections))
} recover {
Expand All @@ -34,6 +35,7 @@ class ImageCollectionsController(authenticated: Authentication, config: Collecti
}

def addCollection(id: String) = authenticated.async(parse.json) { req =>
implicit val instance: Instance = instanceOf(req)
(req.body \ "data").asOpt[List[String]].map { path =>
val collection = Collection.build(path, ActionData(getIdentity(req.user), DateTime.now()))
imageCollectionsStore.add(id, collection)
Expand All @@ -44,6 +46,7 @@ class ImageCollectionsController(authenticated: Authentication, config: Collecti


def removeCollection(id: String, collectionString: String) = authenticated.async { req =>
implicit val instance: Instance = instanceOf(req)
val path = CollectionsManager.uriToPath(UriOps.encodePlus(collectionString))
// We do a get to be able to find the index of the current collection, then remove it.
// Given that we're using Dynamo Lists this seemed like a decent way to do it.
Expand All @@ -63,9 +66,9 @@ class ImageCollectionsController(authenticated: Authentication, config: Collecti
}
}

def publish(id: String)(collections: List[Collection]): List[Collection] = {
def publish(id: String)(collections: List[Collection])(implicit instance: Instance): List[Collection] = {
val onlyLatestCollections = onlyLatest(collections)
val updateMessage = UpdateMessage(subject = SetImageCollections, id = Some(id), collections = Some(onlyLatestCollections))
val updateMessage = UpdateMessage(subject = SetImageCollections, id = Some(id), collections = Some(onlyLatestCollections), instance = instance)
notifications.publish(updateMessage)
onlyLatestCollections
}
Expand Down
3 changes: 2 additions & 1 deletion collections/app/lib/CollectionsConfig.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package lib

import com.gu.mediaservice.lib.config.{CommonConfig, GridConfigResources}
import com.gu.mediaservice.model.Instance


class CollectionsConfig(resources: GridConfigResources) extends CommonConfig(resources) {
val collectionsTable = string("dynamo.table.collections")
val imageCollectionsTable = string("dynamo.table.imageCollections")

val rootUri = services.collectionsBaseUri
val rootUri: Instance => String = services.collectionsBaseUri
}
25 changes: 12 additions & 13 deletions collections/app/store/CollectionsStore.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package store

import cats.implicits._
import com.gu.mediaservice.lib.collections.CollectionsManager
import com.gu.mediaservice.model.{ActionData, Collection}
import com.gu.mediaservice.model.{ActionData, Collection, Instance}
import org.joda.time.DateTime
import org.scanamo.generic.auto.genericDerivedFormat
import org.scanamo.{DynamoFormat, ScanamoAsync, Table}
import org.scanamo.syntax._
import org.scanamo.{DynamoFormat, ScanamoAsync, Table}
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import cats.implicits._
import org.scanamo.generic.semiauto.FieldName

case class Record(id: String, collection: Collection)
case class Record(id: String, collection: Collection, instance: String)

class CollectionsStore(val tableName: String, client: DynamoDbAsyncClient) extends DynamoHelpers {
import org.scanamo.generic.semiauto._
Expand All @@ -25,24 +24,24 @@ class CollectionsStore(val tableName: String, client: DynamoDbAsyncClient) exten

private lazy val collectionsTable = Table[Record](tableName)

def getAll: Future[List[Collection]] = {
ScanamoAsync(client).exec(collectionsTable.scan()).map(_.sequence).flatMap(res =>
def getAll(implicit instance: Instance): Future[List[Collection]] = {
ScanamoAsync(client).exec(collectionsTable.query("instance" === instance.id)).map(_.sequence).flatMap(res =>
handleResponse(res)(records => records.map(_.collection))
)
}

def add(collection: Collection): Future[Collection] = {
def add(collection: Collection)(implicit instance: Instance): Future[Collection] = {
ScanamoAsync(client).exec(
collectionsTable.update(
"id" === collection.pathId,
"id" === collection.pathId and "instance" === instance.id,
set("collection", collection)
)
).flatMap(res => handleResponse(res)(record => record.collection))
}

def get(collectionPath: List[String]): Future[Option[Collection]] = {
def get(collectionPath: List[String])(implicit instance: Instance): Future[Option[Collection]] = {
val path = CollectionsManager.pathToPathId(collectionPath)
ScanamoAsync(client).exec(collectionsTable.get("id" === path)).flatMap(maybeEither =>
ScanamoAsync(client).exec(collectionsTable.get("id" === path and "instance" === instance.id)).flatMap(maybeEither =>
maybeEither.fold[Future[Option[Collection]]](
Future.successful(None)
)(res =>
Expand All @@ -51,9 +50,9 @@ class CollectionsStore(val tableName: String, client: DynamoDbAsyncClient) exten
)
}

def remove(collectionPath: List[String]): Future[Unit] = {
def remove(collectionPath: List[String])(implicit instance: Instance): Future[Unit] = {
val path = CollectionsManager.pathToPathId(collectionPath)
ScanamoAsync(client).exec(collectionsTable.delete("id" === path))
ScanamoAsync(client).exec(collectionsTable.delete("id" === path and "instance" === instance.id))
}
}

Expand Down
Loading
Loading