diff --git a/it/src/main/scala/org/mbari/oni/endpoints/HistoryEndpointsSuite.scala b/it/src/main/scala/org/mbari/oni/endpoints/HistoryEndpointsSuite.scala index 8b9a48b..31aee0d 100644 --- a/it/src/main/scala/org/mbari/oni/endpoints/HistoryEndpointsSuite.scala +++ b/it/src/main/scala/org/mbari/oni/endpoints/HistoryEndpointsSuite.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2024 Monterey Bay Aquarium Research Institute + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.mbari.oni.endpoints import org.mbari.oni.domain.ExtendedHistory diff --git a/oni/src/main/java/org/mbari/oni/jpa/entities/UserAccountEntity.java b/oni/src/main/java/org/mbari/oni/jpa/entities/UserAccountEntity.java index 05eda18..61abab6 100644 --- a/oni/src/main/java/org/mbari/oni/jpa/entities/UserAccountEntity.java +++ b/oni/src/main/java/org/mbari/oni/jpa/entities/UserAccountEntity.java @@ -87,7 +87,7 @@ public class UserAccountEntity implements Serializable, IPersistentObject { nullable = false, length = 50 ) - String password; + String encryptedPassword; @Column( name = "Role", @@ -109,7 +109,7 @@ public class UserAccountEntity implements Serializable, IPersistentObject { String userName; public boolean authenticate(String unencryptedPassword) { - return (new BasicPasswordEncryptor()).checkPassword(unencryptedPassword, password); + return (new BasicPasswordEncryptor()).checkPassword(unencryptedPassword, encryptedPassword); } @Override @@ -150,8 +150,8 @@ public String getLastName() { return lastName; } - public String getPassword() { - return password; + public String getEncryptedPassword() { + return encryptedPassword; } public String getRole() { @@ -202,8 +202,12 @@ public void setLastName(String lastName) { this.lastName = lastName; } - public void setPassword(String unencryptedPassword) { - this.password = (new BasicPasswordEncryptor()).encryptPassword(unencryptedPassword); + public void setEncryptedPassword(String encryptedPassword) { + this.encryptedPassword = encryptedPassword; + } + + public void setPassword(String password) { + this.encryptedPassword = (new BasicPasswordEncryptor()).encryptPassword(password); } public void setRole(String role) { diff --git a/oni/src/main/java/org/mbari/oni/jpa/repositories/PrefNodeRepository.java b/oni/src/main/java/org/mbari/oni/jpa/repositories/PrefNodeRepository.java new file mode 100644 index 0000000..ae4cf98 --- /dev/null +++ b/oni/src/main/java/org/mbari/oni/jpa/repositories/PrefNodeRepository.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.jpa.repositories; + +import jakarta.persistence.EntityManager; +import org.mbari.oni.jpa.entities.PreferenceNodeEntity; + +import java.util.List; +import java.util.Optional; + +public class PrefNodeRepository extends Repository { + + public PrefNodeRepository(EntityManager entityManager) { + super(entityManager); + } + + public Optional findByNodeNameAndPrefKey(String name, String key) { + return findByNamedQuery("PreferenceNode.findByNodeNameAndPrefKey", Map.of("nodeName", name, "prefKey", key)) + .stream() + .findFirst(); + } + + public PreferenceNodeEntity create(String name, String key, String value) { + var prefNode = new PreferenceNodeEntity(); + prefNode.setName(name); + prefNode.setKey(key); + prefNode.setValue(value); + return entityManager.persist(prefNode); + } + + public Optional update(String name, String key, String value) { + var prefNode = findByNodeNameAndPrefKey(name, key); + prefNode.setValue(value); + return entityManager.merge(prefNode); + } + + public void delete(String name, String key) { + var opt = findByNodeNameAndPrefKey(name, key); + opt.ifPresent(prefNode -> entityManager.remove(prefNode)); + } + + public List findByNodeName(String name) { + return findByNamedQuery("PreferenceNode.findAllByNodeName", Map.of("nodeName", name)); + } + + public List findByNodeNameLike(String name) { + return findByNamedQuery("PreferenceNode.findAllLikeNodeName", Map.of("nodeName", name + '%')); + } +} diff --git a/oni/src/main/java/org/mbari/oni/jpa/repositories/Repository.java b/oni/src/main/java/org/mbari/oni/jpa/repositories/Repository.java index 63c76a8..9991ed4 100644 --- a/oni/src/main/java/org/mbari/oni/jpa/repositories/Repository.java +++ b/oni/src/main/java/org/mbari/oni/jpa/repositories/Repository.java @@ -50,6 +50,13 @@ public void delete(Object object) { entityManager.remove(object); } + public void create(Object object) { + entityManager.persist(object); + } + + public void update(Object object) { + entityManager.merge(object); + } public List findByNamedQuery(String name) { Query query = entityManager.createNamedQuery(name); @@ -75,4 +82,6 @@ public Optional findByPrimaryKey(Class clazz, Object primaryKey) { } + + } diff --git a/oni/src/main/scala/org/mbari/oni/Endpoints.scala b/oni/src/main/scala/org/mbari/oni/Endpoints.scala index 43b6908..c32f8a8 100644 --- a/oni/src/main/scala/org/mbari/oni/Endpoints.scala +++ b/oni/src/main/scala/org/mbari/oni/Endpoints.scala @@ -26,7 +26,6 @@ object Endpoints: val historyEndpoints: HistoryEndpoints = HistoryEndpoints(entityMangerFactory) val phylogenyEndpoints: PhylogenyEndpoints = PhylogenyEndpoints(entityMangerFactory) - val authorizationEndpoints: AuthorizationEndpoints = AuthorizationEndpoints() val healthEndpoints: HealthEndpoints = HealthEndpoints() diff --git a/oni/src/main/scala/org/mbari/oni/domain/ConceptMetadata.scala b/oni/src/main/scala/org/mbari/oni/domain/ConceptMetadata.scala index 8611469..d467773 100644 --- a/oni/src/main/scala/org/mbari/oni/domain/ConceptMetadata.scala +++ b/oni/src/main/scala/org/mbari/oni/domain/ConceptMetadata.scala @@ -29,12 +29,10 @@ object ConceptMetadata: def from(concept: ConceptEntity): ConceptMetadata = val name = concept.getPrimaryConceptName.getName - val alternateNames = concept - .getConceptNames + val alternateNames = concept.getAlternativeConceptNames .asScala - .filter(_.getNameType != ConceptNameTypes.PRIMARY.getType) - .toSet .map(_.getName) + .toSet val media = concept .getConceptMetadata diff --git a/oni/src/main/scala/org/mbari/oni/domain/PrefNode.scala b/oni/src/main/scala/org/mbari/oni/domain/PrefNode.scala new file mode 100644 index 0000000..070ce81 --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/domain/PrefNode.scala @@ -0,0 +1,25 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.domain + +import org.mbari.oni.jpa.entities.PreferenceNodeEntity + +case class PrefNode(name: String, key: String, value: String): + def toEntity: PreferenceNodeEntity = + val entity = new PreferenceNodeEntity() + entity.setNodeName(name) + entity.setPrefKey(key) + entity.setPrefValue(value) + entity + +object PrefNode: + def from(entity: PreferenceNodeEntity): PrefNode = PrefNode( + entity.getNodeName, + entity.getPrefKey, + entity.getPrefValue + ) diff --git a/oni/src/main/scala/org/mbari/oni/domain/UserAccount.scala b/oni/src/main/scala/org/mbari/oni/domain/UserAccount.scala new file mode 100644 index 0000000..f24ae75 --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/domain/UserAccount.scala @@ -0,0 +1,59 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.domain + +import org.jasypt.util.password.BasicPasswordEncryptor +import org.mbari.oni.jpa.entities.UserAccountEntity +import org.mbari.oni.etc.jdk.Numbers.given + +case class UserAccount( + username: String, + password: String, + role: String = "ReadOnly", + affiliation: Option[String] = None, + firstName: Option[String] = None, + lastName: Option[String] = None, + email: Option[String] = None, + id: Option[Long] = None, + isEncrypted: Option[Boolean] = None + ) { + + def toEntity: UserAccountEntity = { + val entity = new UserAccountEntity() + entity.setUserName(username) + // If the password is not encrypted, then encrypt it + if (isEncrypted.getOrElse(false)) + entity.setEncryptedPassword(password) + else + entity.setPassword(password) + entity.setRole(role) + entity.setAffiliation(affiliation.orNull) + entity.setFirstName(firstName.orNull) + entity.setLastName(lastName.orNull) + entity.setEmail(email.orNull) + entity.setId(id.map(_.asInstanceOf[java.lang.Long]).orNull) + entity + } + +} + +object UserAccount { + + def from(userAccount: UserAccountEntity): UserAccount = UserAccount( + userAccount.getUserName, + userAccount.getEncryptedPassword, + userAccount.getRole, + Option(userAccount.getAffiliation), + Option(userAccount.getFirstName), + Option(userAccount.getLastName), + Option(userAccount.getEmail), + Option(userAccount.getPrimaryKey).map(_.asInstanceOf[Long]), + Some(true) + ) + +} diff --git a/oni/src/main/scala/org/mbari/oni/domain/UserAccountUpdate.scala b/oni/src/main/scala/org/mbari/oni/domain/UserAccountUpdate.scala new file mode 100644 index 0000000..254f84f --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/domain/UserAccountUpdate.scala @@ -0,0 +1,20 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.domain + +case class UserAccountUpdate(username: String, + password: Option[String] = None, + role: Option[String] = None, + affiliation: Option[String] = None, + firstName: Option[String] = None, + lastName: Option[String] = None, + email: Option[String] = None, + ) { + + +} diff --git a/oni/src/main/scala/org/mbari/oni/endpoints/HistoryEndpoints.scala b/oni/src/main/scala/org/mbari/oni/endpoints/HistoryEndpoints.scala index b1e66f3..8e19d3f 100644 --- a/oni/src/main/scala/org/mbari/oni/endpoints/HistoryEndpoints.scala +++ b/oni/src/main/scala/org/mbari/oni/endpoints/HistoryEndpoints.scala @@ -1,3 +1,10 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + package org.mbari.oni.endpoints import jakarta.persistence.EntityManagerFactory diff --git a/oni/src/main/scala/org/mbari/oni/endpoints/LinkEndpoints.scala b/oni/src/main/scala/org/mbari/oni/endpoints/LinkEndpoints.scala new file mode 100644 index 0000000..03cffdc --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/endpoints/LinkEndpoints.scala @@ -0,0 +1,91 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.endpoints + +import jakarta.persistence.EntityManagerFactory +import org.mbari.oni.domain.{ErrorMsg, Link} +import org.mbari.oni.services.LinkService +import sttp.tapir.* +import sttp.tapir.Endpoint +import sttp.tapir.json.circe.* +import sttp.tapir.server.ServerEndpoint +import sttp.tapir.server.nima.Id +import org.mbari.oni.etc.circe.CirceCodecs.given + +class LinkEndpoints(entityManagerFactory: EntityManagerFactory) extends Endpoints { + + private val service = LinkService(entityManagerFactory) + private val base = "links" + private val tag = "Links" + // get all links + + val allLinksEndpoint: Endpoint[Unit, Unit, ErrorMsg, Seq[Link], Any] = openEndpoint + .get + .in(base) + .out(jsonBody[Seq[Link]]) + .name("links") + .description("Get all links") + .tag(tag) + + val allLinksEndpointImpl: ServerEndpoint[Any, Id] = allLinksEndpoint.serverLogic { + _ => handleErrors(service.findAllLinkTemplates()) + } + + // get links for a concept + val linksForConceptEndpoint: Endpoint[Unit, String, ErrorMsg, Seq[Link], Any] = openEndpoint + .get + .in(base / path[String]("name")) + .out(jsonBody[Seq[Link]]) + .name("linksForConcept") + .description("Get all link templates applicable to a concept") + .tag(tag) + + val linksForConceptEndpointImpl: ServerEndpoint[Any, Id] = linksForConceptEndpoint.serverLogic { name => + handleErrors(service.findAllLinkTemplatesForConcept(name)) + } + + // get links for a concept and linkname + val linksForConceptAndLinkNameEndpoint: Endpoint[Unit, (String, String), ErrorMsg, Seq[Link], Any] = openEndpoint + .get + .in(base / path[String]("name") / "using" / path[String]("linkName")) + .out(jsonBody[Seq[Link]]) + .name("linksForConceptAndLinkName") + .description("Get all link templates applicable to a concept and link name") + .tag(tag) + + val linksForConceptAndLinkNameEndpointImpl: ServerEndpoint[Any, Id] = linksForConceptAndLinkNameEndpoint.serverLogic { (name, linkName) => + handleErrors(service.findLinkTemplatesByNameForConcept(name, linkName)) + } + + // get link realizations for a concept + val linkRealizationsEndpoint: Endpoint[Unit, String, ErrorMsg, Seq[Link], Any] = openEndpoint + .get + .in(base / "query" / "linkrealizations" / path[String]("linkName")) + .out(jsonBody[Seq[Link]]) + .name("linkRealizations") + .description("Get all link realizations for a link name") + .tag(tag) + + val linkRealizationsEndpointImpl: ServerEndpoint[Any, Id] = linkRealizationsEndpoint.serverLogic { linkName => + handleErrors(service.findLinkRealizationsByLinkName(linkName)) + } + + override def all: List[Endpoint[_, _, _, _, _]] = List ( + linkRealizationsEndpoint, + linksForConceptAndLinkNameEndpoint, + linksForConceptEndpoint, // TODO verify this order works + allLinksEndpoint, + ) + + override def allImpl: List[ServerEndpoint[Any, Id]] = List( + linkRealizationsEndpointImpl, + linksForConceptAndLinkNameEndpointImpl, + linksForConceptEndpointImpl, + allLinksEndpointImpl, + ) +} diff --git a/oni/src/main/scala/org/mbari/oni/endpoints/RawEndpoints.scala b/oni/src/main/scala/org/mbari/oni/endpoints/RawEndpoints.scala new file mode 100644 index 0000000..1547e7f --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/endpoints/RawEndpoints.scala @@ -0,0 +1,12 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.endpoints + +class RawEndpoints { + +} diff --git a/oni/src/main/scala/org/mbari/oni/services/PrefNodeService.scala b/oni/src/main/scala/org/mbari/oni/services/PrefNodeService.scala new file mode 100644 index 0000000..ca6e7a1 --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/services/PrefNodeService.scala @@ -0,0 +1,77 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.services + +import jakarta.persistence.EntityManagerFactory +import org.mbari.oni.domain.PrefNode +import org.mbari.oni.jpa.EntityManagerFactories.* +import org.mbari.oni.jpa.entities.PreferenceNodeEntity +import org.mbari.oni.jpa.repositories.{PrefNodeRepository, VarsUserPreferencesFactory} +import scala.jdk.OptionConverters.* +import scala.jdk.CollectionConverters.* + +class PrefNodeService(entityManagerFactory: EntityManagerFactory) { + + + def create(name: String, key: String, value: String): Either[Throwable, PrefNode] = { + val entity = new PreferenceNodeEntity() + entity.setNodeName(name) + entity.setPrefKey(key) + entity.setPrefValue(value) + entityManagerFactory.transaction(entityManager => + val repo = new PrefNodeRepository(entityManager) + repo.create(entity) + PrefNode.from(entity) + ) + + } + + def update(name: String, key: String, value: String): Either[Throwable, PrefNode] = { + entityManagerFactory.transaction(entityManager => + val repo = new PrefNodeRepository(entityManager) + repo.findByNodeNameAndPrefKey(name, key).toScala match + case Some(entity) => + entity.setPrefValue(value) + repo.update(entity) + PrefNode.from(entity) + case None => + throw new IllegalArgumentException(s"PrefNode with name ${name} and key ${key} does not exist") + ) + } + + def delete(name: String, key: String): Unit = { + entityManagerFactory.transaction(entityManager => + val repo = new PrefNodeRepository(entityManager) + repo.findByNodeNameAndPrefKey(name, key).toScala match + case Some(entity) => repo.delete(entity) + case None => throw new IllegalArgumentException(s"PrefNode with name ${name} and key ${key} does not exist") + ) + } + + def findByNodeNameAndKey(name: String, key: String): Either[Throwable, Option[PrefNode]] = { + entityManagerFactory.transaction(entityManager => + val repo = new PrefNodeRepository(entityManager) + repo.findByNodeNameAndPrefKey(name, key).map(PrefNode.from).toScala + ) + } + + def findByNodeName(name: String): Either[Throwable, Seq[PrefNode]] = { + entityManagerFactory.transaction(entityManager => + val repo = new PrefNodeRepository(entityManager) + repo.findByNodeName(name).asScala.map(PrefNode.from).toSeq + ) + } + + def findByNodeNameLike(name: String): Either[Throwable, Seq[PrefNode]] = { + entityManagerFactory.transaction(entityManager => + val repo = new PrefNodeRepository(entityManager) + repo.findByNodeNameLike(name).asScala.map(PrefNode.from).toSeq + ) + } + +} diff --git a/oni/src/main/scala/org/mbari/oni/services/UserAccountService.scala b/oni/src/main/scala/org/mbari/oni/services/UserAccountService.scala new file mode 100644 index 0000000..e672d2d --- /dev/null +++ b/oni/src/main/scala/org/mbari/oni/services/UserAccountService.scala @@ -0,0 +1,86 @@ +/* + * Copyright (c) Monterey Bay Aquarium Research Institute 2024 + * + * oni code is non-public software. Unauthorized copying of this file, + * via any medium is strictly prohibited. Proprietary and confidential. + */ + +package org.mbari.oni.services + +import jakarta.persistence.EntityManagerFactory +import org.mbari.oni.domain.{UserAccount, UserAccountUpdate} +import org.mbari.oni.jpa.EntityManagerFactories.* +import org.mbari.oni.jpa.repositories.UserAccountRepository + +import scala.jdk.CollectionConverters.* +import scala.jdk.OptionConverters.* + +class UserAccountService(entityManagerFactory: EntityManagerFactory) { + + def findAll(): Either[Throwable, Seq[UserAccount]] = { + entityManagerFactory.transaction(entityManager => + val repo = UserAccountRepository(entityManager) + repo.findAll() + .asScala + .toSeq + .map(UserAccount.from) + ) + } + + def findByUserName(name: String): Either[Throwable, Option[UserAccount]] = { + entityManagerFactory.transaction(entityManager => + val repo = UserAccountRepository(entityManager) + repo.findByUserName(name) + .map(UserAccount.from) + .toScala + ) + } + + def findAllByRole(role: String): Either[Throwable, Seq[UserAccount]] = { + entityManagerFactory.transaction(entityManager => + val repo = UserAccountRepository(entityManager) + repo.findAllByRole(role) + .asScala + .toSeq + .map(UserAccount.from) + ) + } + + def deleteByUserName(name: String): Either[Throwable, Unit] = { + entityManagerFactory.transaction(entityManager => + val repo = UserAccountRepository(entityManager) + repo.findByUserName(name).toScala match + case Some(entity) => repo.delete(entity) + case None => throw new IllegalArgumentException(s"UserAccount with username ${name} does not exist") + ) + } + + def create(userAccount: UserAccount): Either[Throwable, UserAccount] = { + entityManagerFactory.transaction(entityManager => + val repo = UserAccountRepository(entityManager) + repo.findByUserName(userAccount.username).toScala match + case Some(_) => throw new IllegalArgumentException(s"UserAccount with username ${userAccount.username} already exists") + case None => + val entity = userAccount.toEntity + repo.create(entity) + UserAccount.from(entity) + ) + } + + def update(userAccount: UserAccountUpdate): Either[Throwable, UserAccount] = { + entityManagerFactory.transaction(entityManager => + val repo = UserAccountRepository(entityManager) + repo.findByUserName(userAccount.username).toScala match + case Some(entity) => + userAccount.password.foreach(entity.setPassword) + userAccount.role.foreach(entity.setRole) + userAccount.affiliation.foreach(entity.setAffiliation) + userAccount.firstName.foreach(entity.setFirstName) + userAccount.lastName.foreach(entity.setLastName) + userAccount.email.foreach(entity.setEmail) + UserAccount.from(entity) + case None => throw new IllegalArgumentException(s"UserAccount with username ${userAccount.username} does not exist") + ) + } + +}