Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import kotlin.time.Instant
data class Collection(
val id: Long,
val name: String,
val ownedBy: String,
val ownedBy: Long,
val organism: String,
val description: String?,
val variants: List<Variant>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.genspectrum.dashboardsbackend.api

import kotlin.time.Instant

data class User(
val id: Long,
val githubId: String?,
val name: String,
val email: String?,
val createdAt: Instant,
val updatedAt: Instant,
)

data class UserSyncRequest(val githubId: String, val name: String, val email: String?)

data class PublicUser(val id: Long, val name: String)
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class CollectionsController(private val collectionModel: CollectionModel) {
description = "Returns collections filtered by optional userId and/or organism parameters.",
)
fun getCollections(
@RequestParam(required = false) userId: String?,
@RequestParam(required = false) userId: Long?,
@RequestParam(required = false) organism: String?,
): List<Collection> = collectionModel.getCollections(
userId = userId,
Expand All @@ -50,7 +50,7 @@ class CollectionsController(private val collectionModel: CollectionModel) {
)
fun postCollection(
@RequestBody collection: CollectionRequest,
@UserIdParameter @RequestParam userId: String,
@UserIdParameter @RequestParam userId: Long,
): Collection = collectionModel.createCollection(
request = collection,
userId = userId,
Expand All @@ -65,7 +65,7 @@ class CollectionsController(private val collectionModel: CollectionModel) {
fun putCollection(
@RequestBody collection: CollectionUpdate,
@Parameter(description = "The ID of the collection", example = "1") @PathVariable id: Long,
@UserIdParameter @RequestParam userId: String,
@UserIdParameter @RequestParam userId: Long,
): Collection = collectionModel.putCollection(id, collection, userId)

@DeleteMapping("/collections/{id}")
Expand All @@ -76,6 +76,6 @@ class CollectionsController(private val collectionModel: CollectionModel) {
)
fun deleteCollection(
@Parameter(description = "The ID of the collection", example = "1") @PathVariable id: Long,
@UserIdParameter @RequestParam userId: String,
@UserIdParameter @RequestParam userId: Long,
) = collectionModel.deleteCollection(id, userId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class SubscriptionsController(
)
fun getSubscription(
@IdParameter @PathVariable id: String,
@UserIdParameter @RequestParam userId: String,
@UserIdParameter @RequestParam userId: Long,
): Subscription = subscriptionModel.getSubscription(
subscriptionId = id,
userId = userId,
Expand All @@ -47,7 +47,7 @@ class SubscriptionsController(
summary = "Get all subscriptions of a user",
description = "Returns a list of all subscriptions of a user.",
)
fun getSubscriptions(@UserIdParameter @RequestParam userId: String): List<Subscription> =
fun getSubscriptions(@UserIdParameter @RequestParam userId: Long): List<Subscription> =
subscriptionModel.getSubscriptions(userId)

@PostMapping("/subscriptions")
Expand All @@ -58,7 +58,7 @@ class SubscriptionsController(
)
fun postSubscriptions(
@RequestBody subscription: SubscriptionRequest,
@UserIdParameter @RequestParam userId: String,
@UserIdParameter @RequestParam userId: Long,
): Subscription = subscriptionModel.postSubscriptions(
request = subscription,
userId = userId,
Expand All @@ -70,7 +70,7 @@ class SubscriptionsController(
summary = "Delete a subscription",
description = "Deletes a specific subscription of a user by its uuid.",
)
fun deleteSubscription(@IdParameter @PathVariable id: String, @UserIdParameter @RequestParam userId: String) {
fun deleteSubscription(@IdParameter @PathVariable id: String, @UserIdParameter @RequestParam userId: Long) {
subscriptionModel.deleteSubscription(
subscriptionId = id,
userId = userId,
Expand All @@ -85,7 +85,7 @@ class SubscriptionsController(
fun putSubscription(
@RequestBody subscription: SubscriptionUpdate,
@IdParameter @PathVariable id: String,
@UserIdParameter @RequestParam userId: String,
@UserIdParameter @RequestParam userId: Long,
): Subscription = subscriptionModel.putSubscription(
subscriptionId = id,
subscriptionUpdate = subscription,
Expand All @@ -99,7 +99,7 @@ class SubscriptionsController(
)
fun evaluateTrigger(
@IdParameter @RequestParam id: String,
@UserIdParameter @RequestParam userId: String,
@UserIdParameter @RequestParam userId: Long,
): TriggerEvaluationResponse {
val triggerEvaluationResult = triggerEvaluationModel.evaluateSubscriptionTrigger(
subscriptionId = id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.genspectrum.dashboardsbackend.controller

import io.swagger.v3.oas.annotations.Operation
import org.genspectrum.dashboardsbackend.api.PublicUser
import org.genspectrum.dashboardsbackend.api.User
import org.genspectrum.dashboardsbackend.api.UserSyncRequest
import org.genspectrum.dashboardsbackend.model.user.UserModel
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class UsersController(private val userModel: UserModel) {
@PostMapping("/users/sync", produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation(
summary = "Sync user from external auth provider",
description = "Upserts a user record by github_id. Returns the user with their internal ID.",
)
fun syncUser(@RequestBody request: UserSyncRequest): User = userModel.syncUser(request)

@GetMapping("/users/{id}", produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation(
summary = "Get user by internal ID",
description = "Returns public user info by internal UUID.",
)
fun getUser(@PathVariable id: Long): PublicUser = userModel.getUser(id)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.genspectrum.dashboardsbackend.config.DashboardsConfig
import org.genspectrum.dashboardsbackend.controller.BadRequestException
import org.genspectrum.dashboardsbackend.controller.ForbiddenException
import org.genspectrum.dashboardsbackend.controller.NotFoundException
import org.genspectrum.dashboardsbackend.util.now
import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
Expand All @@ -18,19 +19,12 @@ import org.jetbrains.exposed.v1.core.notInList
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import kotlin.time.Clock
import kotlin.time.Instant

@Service
@Transactional
class CollectionModel(private val dashboardsConfig: DashboardsConfig) {
// Truncate to milliseconds to avoid mismatches between the in-memory value
// we return and what is read back from the DB.
private fun now(): Instant = Clock.System.now().run {
Instant.fromEpochMilliseconds(toEpochMilliseconds())
}

fun getCollections(userId: String?, organism: String?): List<Collection> {
fun getCollections(userId: Long?, organism: String?): List<Collection> {
if (organism != null) {
dashboardsConfig.validateIsValidOrganism(organism)
dashboardsConfig.validateCollectionsEnabled(organism)
Expand Down Expand Up @@ -81,7 +75,7 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) {
return entity.toCollection()
}

fun createCollection(request: CollectionRequest, userId: String): Collection {
fun createCollection(request: CollectionRequest, userId: Long): Collection {
dashboardsConfig.validateIsValidOrganism(request.organism)
dashboardsConfig.validateCollectionsEnabled(request.organism)

Expand Down Expand Up @@ -113,7 +107,7 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) {
)
}

fun deleteCollection(id: Long, userId: String) {
fun deleteCollection(id: Long, userId: Long) {
// Find with ownership check
val entity = CollectionEntity.findForUser(id, userId)
?: throw ForbiddenException("Collection $id not found or you don't have permission to delete it")
Expand All @@ -122,7 +116,7 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) {
entity.delete()
}

fun putCollection(id: Long, update: CollectionUpdate, userId: String): Collection {
fun putCollection(id: Long, update: CollectionUpdate, userId: Long): Collection {
val collectionEntity = CollectionEntity.findForUser(id, userId)
?: throw ForbiddenException("Collection $id not found or you don't have permission to update it")
dashboardsConfig.validateCollectionsEnabled(collectionEntity.organism)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const val COLLECTION_TABLE = "collections_table"

object CollectionTable : LongIdTable(COLLECTION_TABLE) {
val name = text("name")
val ownedBy = varchar("owned_by", 255)
val ownedBy = long("owned_by")
val organism = varchar("organism", 255)
val description = text("description").nullable()
val createdAt = timestamp("created_at")
Expand All @@ -20,7 +20,7 @@ object CollectionTable : LongIdTable(COLLECTION_TABLE) {

class CollectionEntity(id: EntityID<Long>) : LongEntity(id) {
companion object : LongEntityClass<CollectionEntity>(CollectionTable) {
fun findForUser(id: Long, userId: String) = findById(id)
fun findForUser(id: Long, userId: Long) = findById(id)
?.takeIf { it.ownedBy == userId }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ import org.springframework.transaction.annotation.Transactional
@Service
@Transactional
class SubscriptionModel(private val dashboardsConfig: DashboardsConfig) {
fun getSubscription(subscriptionId: String, userId: String): Subscription =
fun getSubscription(subscriptionId: String, userId: Long): Subscription =
SubscriptionEntity.findForUser(convertToUuid(subscriptionId), userId)
?.toSubscription()
?: throw NotFoundException("Subscription $subscriptionId not found")

fun getSubscriptions(userId: String): List<Subscription> = SubscriptionEntity.find {
fun getSubscriptions(userId: Long): List<Subscription> = SubscriptionEntity.find {
SubscriptionTable.userId eq userId
}.map {
it.toSubscription()
}

fun postSubscriptions(request: SubscriptionRequest, userId: String): Subscription {
fun postSubscriptions(request: SubscriptionRequest, userId: Long): Subscription {
dashboardsConfig.validateIsValidOrganism(request.organism)

return SubscriptionEntity
Expand All @@ -40,14 +40,14 @@ class SubscriptionModel(private val dashboardsConfig: DashboardsConfig) {
.toSubscription()
}

fun deleteSubscription(subscriptionId: String, userId: String) {
fun deleteSubscription(subscriptionId: String, userId: Long) {
val subscription = SubscriptionEntity.findForUser(convertToUuid(subscriptionId), userId)
?: throw NotFoundException("Subscription $subscriptionId not found")

subscription.delete()
}

fun putSubscription(subscriptionId: String, subscriptionUpdate: SubscriptionUpdate, userId: String): Subscription {
fun putSubscription(subscriptionId: String, subscriptionUpdate: SubscriptionUpdate, userId: Long): Subscription {
subscriptionUpdate.organism?.also { dashboardsConfig.validateIsValidOrganism(it) }

val subscription = SubscriptionEntity.findForUser(convertToUuid(subscriptionId), userId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ object SubscriptionTable : UUIDTable(SUBSCRIPTION_TABLE) {
val organism = varchar("organism", 255)
val dateWindow = varchar("date_window", 255)
val trigger = jacksonSerializableJsonb<Trigger>("trigger")
val userId = varchar("user_id", 255)
val userId = long("user_id")
}

class SubscriptionEntity(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<SubscriptionEntity>(SubscriptionTable) {
fun findForUser(id: UUID, userId: String) = findById(id)
fun findForUser(id: UUID, userId: Long) = findById(id)
?.takeIf { it.userId == userId }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class TriggerEvaluationModel(
private val subscriptionModel: SubscriptionModel,
private val triggerEvaluator: TriggerEvaluator,
) {
fun evaluateSubscriptionTrigger(subscriptionId: String, userId: String) = triggerEvaluator.evaluate(
fun evaluateSubscriptionTrigger(subscriptionId: String, userId: Long) = triggerEvaluator.evaluate(
subscriptionModel.getSubscription(
subscriptionId = subscriptionId,
userId = userId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.genspectrum.dashboardsbackend.model.user

import org.genspectrum.dashboardsbackend.api.PublicUser
import org.genspectrum.dashboardsbackend.api.User
import org.genspectrum.dashboardsbackend.api.UserSyncRequest
import org.genspectrum.dashboardsbackend.controller.NotFoundException
import org.genspectrum.dashboardsbackend.util.now
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class UserModel {
fun syncUser(request: UserSyncRequest): User {
val now = now()
val existing = UserEntity.findByGithubId(request.githubId)
return if (existing != null) {
existing.name = request.name
existing.email = request.email
existing.updatedAt = now
existing.toUser()
} else {
UserEntity.new {
githubId = request.githubId
name = request.name
email = request.email
createdAt = now
updatedAt = now
}.toUser()
}
}

fun getUser(id: Long): PublicUser = UserEntity.findById(id)?.toPublicUser()
?: throw NotFoundException("User $id not found")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.genspectrum.dashboardsbackend.model.user

import org.genspectrum.dashboardsbackend.api.PublicUser
import org.genspectrum.dashboardsbackend.api.User
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.dao.id.LongIdTable
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.dao.LongEntity
import org.jetbrains.exposed.v1.dao.LongEntityClass
import org.jetbrains.exposed.v1.datetime.timestamp

const val USER_TABLE = "users_table"

object UserTable : LongIdTable(USER_TABLE) {
val githubId = varchar("github_id", 255).nullable().uniqueIndex()
val name = varchar("name", 255)
val email = varchar("email", 255).nullable()
val createdAt = timestamp("created_at")
val updatedAt = timestamp("updated_at")
}

class UserEntity(id: EntityID<Long>) : LongEntity(id) {
companion object : LongEntityClass<UserEntity>(UserTable) {
fun findByGithubId(githubId: String) = find { UserTable.githubId eq githubId }.firstOrNull()
}

var githubId by UserTable.githubId
var name by UserTable.name
var email by UserTable.email
var createdAt by UserTable.createdAt
var updatedAt by UserTable.updatedAt

fun toPublicUser() = PublicUser(id = id.value, name = name)

fun toUser() = User(
id = id.value,
githubId = githubId,
name = name,
email = email,
createdAt = createdAt,
updatedAt = updatedAt,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.genspectrum.dashboardsbackend.util

import kotlin.time.Clock
import kotlin.time.Instant

// Truncate to milliseconds to avoid mismatches between the in-memory value
// we return and what is read back from the DB.
fun now(): Instant = Clock.System.now().run {
Instant.fromEpochMilliseconds(toEpochMilliseconds())
}
Loading
Loading