| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 |
| * |
| * https://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.apache.nlpcraft.server.rest |
| |
| import akka.http.scaladsl.coding.Coders |
| import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ |
| import akka.http.scaladsl.model.HttpMethods._ |
| import akka.http.scaladsl.model._ |
| import akka.http.scaladsl.model.headers._ |
| import akka.http.scaladsl.server.Directives.{entity, _} |
| import akka.http.scaladsl.server._ |
| import com.fasterxml.jackson.core.JsonProcessingException |
| import com.fasterxml.jackson.databind.ObjectMapper |
| import com.fasterxml.jackson.module.scala.DefaultScalaModule |
| import com.typesafe.scalalogging.LazyLogging |
| import io.opencensus.stats.Measure |
| import io.opencensus.trace.{Span, Status} |
| import org.apache.commons.validator.routines.UrlValidator |
| import org.apache.nlpcraft.common.opencensus.NCOpenCensusTrace |
| import org.apache.nlpcraft.common.pool.NCThreadPoolManager |
| import org.apache.nlpcraft.common.util.NCUtils.{jsonToJavaMap, uncompress} |
| import org.apache.nlpcraft.common.{JavaMeta, NCE, U} |
| import org.apache.nlpcraft.model.NCModelView |
| import org.apache.nlpcraft.server.apicodes.NCApiStatusCode.{API_OK, _} |
| import org.apache.nlpcraft.server.company.NCCompanyManager |
| import org.apache.nlpcraft.server.feedback.NCFeedbackManager |
| import org.apache.nlpcraft.server.mdo.{NCQueryStateMdo, NCUserMdo} |
| import org.apache.nlpcraft.server.opencensus.NCOpenCensusServerStats |
| import org.apache.nlpcraft.server.probe.NCProbeManager |
| import org.apache.nlpcraft.server.query.NCQueryManager |
| import org.apache.nlpcraft.server.sugsyn.NCSuggestSynonymManager |
| import org.apache.nlpcraft.server.user.NCUserManager |
| import spray.json.DefaultJsonProtocol._ |
| import spray.json.{JsObject, JsValue, RootJsonFormat} |
| |
| import scala.concurrent.{ExecutionContext, Future} |
| import scala.jdk.CollectionConverters._ |
| |
| /** |
| * REST API default implementation. |
| */ |
| class NCBasicRestApi extends NCRestApi with LazyLogging with NCOpenCensusTrace with NCOpenCensusServerStats { |
| protected final val URL_VALIDATOR = new UrlValidator(Array("http", "https"), UrlValidator.ALLOW_LOCAL_URLS) |
| |
| final val API_VER = 1 |
| final val API = "api" / s"v$API_VER" |
| /** */ |
| private final val CORS_HDRS = List( |
| `Access-Control-Allow-Origin`.*, |
| `Access-Control-Allow-Credentials`(true), |
| `Access-Control-Allow-Headers`("Authorization", "Content-Type", "X-Requested-With") |
| ) |
| |
| private final val JS_MAPPER = new ObjectMapper() |
| |
| JS_MAPPER.registerModule(DefaultScalaModule) |
| |
| /* |
| * General control exception. |
| * Note that these classes must be public because scala 2.11 internal errors (compilations problems). |
| */ |
| case class AccessTokenFailure(acsTkn: String) extends NCE(s"Unknown or expired access token: $acsTkn") |
| case class SignInFailure(email: String) extends NCE(s"Invalid or unknown user email: $email") |
| case class AdminRequired(email: String) extends NCE(s"Admin privileges required: $email") |
| case class InvalidOperation(email: String) extends NCE(s"Invalid operation.") |
| case class NotImplemented() extends NCE("Not implemented.") |
| |
| class InvalidArguments(msg: String) extends NCE(msg) |
| case class OutOfRangeField(fn: String, from: Number, to: Number) extends InvalidArguments(s"Parameter ($fn) value is out of range ($from, $to).") |
| case class TooLargeField(fn: String, max: Int) extends InvalidArguments(s"Parameter ($fn) value exceeded max length of $max.") |
| case class InvalidField(fn: String) extends InvalidArguments(s"Parameter invalid: $fn") |
| case class EmptyField(fn: String) extends InvalidArguments(s"Parameter cannot be empty: $fn") |
| case class InvalidExternalUserId(usrExtId: String) extends InvalidArguments(s"External user ID is invalid or unknown: $usrExtId") |
| case class InvalidUserId(id: Long) extends InvalidArguments(s"User ID is invalid or unknown: $id") |
| case class InvalidModelId(id: String) extends InvalidArguments(s"Unknown model ID: $id") |
| case class InvalidModelOrElementId(mdlId: String, elmId: String) extends InvalidArguments(s"Unknown model element ID: $elmId") |
| |
| case class AskReqHolder( |
| usrId: Long, |
| txt: String, |
| mdlId: String, |
| usrAgent: Option[String], |
| rmtAddr: Option[String], |
| data: Option[String], |
| enableLog: Boolean, |
| parent: Span |
| ) |
| |
| /* |
| * Maximum length specification for frequently used standard REST parameters, |
| */ |
| private final val STD_FIELD_LENGTHS = Map[String, Int]( |
| "srvReqId" -> 32, |
| "mdlId" -> NCModelView.MODEL_ID_MAXLEN, |
| "elmId" -> NCModelView.MODEL_ELEMENT_ID_MAXLEN, |
| "country" -> 32, |
| "postalCode" -> 32, |
| |
| "usrExtId" -> 64, |
| "name" -> 64, |
| "firstName" -> 64, |
| "lastName" -> 64, |
| "passwd" -> 64, |
| "newPasswd" -> 64, |
| "email" -> 64, |
| "adminEmail" -> 64, |
| "adminPasswd" -> 64, |
| "adminFirstName" -> 64, |
| "adminLastName" -> 64, |
| |
| "website" -> 256, |
| "acsTok" -> 256, |
| |
| "city" -> 512, |
| "region" -> 512, |
| "address" -> 512, |
| |
| "txt" -> 1024, |
| "comment" -> 1024, |
| |
| "avatarUrl" -> 512000, |
| "adminAvatarUrl" -> 512000 |
| ) |
| |
| /** |
| * |
| * @param o |
| * @return |
| */ |
| @throws[NCE] |
| private def toJs(o: AnyRef): String = |
| try |
| JS_MAPPER.writeValueAsString(o) |
| catch { |
| case e: JsonProcessingException => throw new NCE(s"JSON serialization error.", e) |
| } |
| |
| /** |
| * |
| * @param acsTkn Access token to check. |
| * @param shouldBeAdmin Admin flag. |
| * @return |
| */ |
| @throws[NCE] |
| private def authenticate0(acsTkn: String, shouldBeAdmin: Boolean): NCUserMdo = |
| startScopedSpan("authenticate0", "acsTkn" -> acsTkn, "shouldBeAdmin" -> shouldBeAdmin) { span => |
| NCUserManager.getUserForAccessToken(acsTkn, span) match { |
| case None => throw AccessTokenFailure(acsTkn) |
| case Some(usr) => |
| require(usr.email.isDefined) |
| |
| if (shouldBeAdmin && !usr.isAdmin) |
| throw AdminRequired(usr.email.get) |
| |
| usr |
| } |
| } |
| |
| /** |
| * |
| * @param rmtAddr |
| * @return |
| */ |
| private def getAddress(rmtAddr: RemoteAddress): Option[String] = |
| rmtAddr.toOption match { |
| // 127.0.0.1 used to avoid local addresses like 0:0:0:0:0:0:0:1 (IPv6) |
| case Some(a) => Some(if (a.getHostName == "localhost") "127.0.0.1" else a.getHostAddress) |
| case None => None |
| } |
| |
| /** |
| * |
| * @param acsUsr |
| * @param srvReqIdsOpt |
| * @param usrIdOpt |
| * @param usrExtIdOpt |
| * @param span |
| */ |
| @throws[AdminRequired] |
| private def getRequests( |
| acsUsr: NCUserMdo, |
| srvReqIdsOpt: Option[Set[String]], |
| usrIdOpt: Option[Long], |
| usrExtIdOpt: Option[String], |
| span: Span |
| ): Set[NCQueryStateMdo] = { |
| require(acsUsr.email.isDefined) |
| |
| val userId = getUserId(acsUsr, usrIdOpt, usrExtIdOpt) |
| |
| val states = srvReqIdsOpt match { |
| case Some(srvReqIds) => |
| val states = NCQueryManager.getForServerRequestIds(srvReqIds, span) |
| |
| if (usrIdOpt.isDefined || usrExtIdOpt.isDefined) states.filter(_.userId == userId) else states |
| case None => NCQueryManager.getForUserId(userId, span) |
| } |
| |
| if (states.exists(_.companyId != acsUsr.companyId) || !acsUsr.isAdmin && states.exists(_.userId != acsUsr.id)) |
| throw AdminRequired(acsUsr.email.get) |
| |
| states |
| } |
| |
| /** |
| * |
| * @param s Query state MDO to convert to map. |
| * @return |
| */ |
| private def queryStateToMap(s: NCQueryStateMdo): java.util.Map[String, Any] = |
| Map( |
| "srvReqId" -> s.srvReqId, |
| "txt" -> s.text, |
| "usrId" -> s.userId, |
| "mdlId" -> s.modelId, |
| "probeId" -> s.probeId.orNull, |
| "status" -> s.status, |
| "resType" -> s.resultType.orNull, |
| "resBody" -> ( |
| if (s.resultBody.isDefined && |
| s.resultType.isDefined && |
| s.resultType.get == "json" |
| ) |
| U.jsonToObject(s.resultBody.get) |
| else |
| s.resultBody.orNull |
| ), |
| "resMeta" -> s.resultMeta.orNull, |
| "error" -> s.error.orNull, |
| "errorCode" -> s.errorCode.map(Integer.valueOf).orNull, |
| "logHolder" -> (if (s.logJson.isDefined) U.jsonToObject(s.logJson.get) else null), |
| "intentId" -> s.intentId.orNull |
| ).filter(_._2 != null).asJava |
| |
| /** |
| * Extracts and checks JSON. |
| * |
| * @param jsOpt JSON value. Optional. |
| * @param name Property name. |
| */ |
| @throws[TooLargeField] |
| @throws[InvalidField] |
| private def extractJson(jsOpt: Option[spray.json.JsValue], name: String): Option[String] = |
| jsOpt match { |
| case Some(js) => |
| val s = js.compactPrint |
| |
| checkLength(name, s, 512000) |
| |
| // Validates. |
| try |
| U.jsonToJavaMap(s) |
| catch { |
| case _: NCE => throw InvalidField(name) |
| } |
| |
| Some(s) |
| case None => None |
| } |
| |
| /** |
| * |
| * @param r Route to CORS enable. |
| */ |
| protected def corsHandler(r: Route): Route = respondWithHeaders(CORS_HDRS) { |
| options { |
| complete(HttpResponse(StatusCodes.OK). |
| withHeaders(`Access-Control-Allow-Methods`(OPTIONS, POST, PUT, GET, DELETE))) |
| } ~ r |
| } |
| |
| /** |
| * Checks operation permissions and gets user ID. |
| * |
| * @param acsUsr Currently signed in user. |
| * @param usrIdOpt User ID. Optional. |
| * @param usrExtIdOpt User 'on-behalf-of' external ID. Optional. |
| */ |
| @throws[AdminRequired] |
| @throws[InvalidUserId] |
| @throws[InvalidExternalUserId] |
| protected def getUserId( |
| acsUsr: NCUserMdo, |
| usrIdOpt: Option[Long], |
| usrExtIdOpt: Option[String] |
| ): Long = { |
| require(acsUsr.email.isDefined) |
| |
| val id1Opt = usrIdOpt match { |
| case Some(userId) => |
| if (!acsUsr.isAdmin && userId != acsUsr.id) |
| throw AdminRequired(acsUsr.email.get) |
| |
| val usr = NCUserManager.getUserById(userId).getOrElse(throw InvalidUserId(userId)) |
| |
| if (usr.companyId != acsUsr.companyId) |
| throw InvalidUserId(userId) |
| |
| Some(userId) |
| |
| case None => None |
| } |
| |
| val id2Opt = usrExtIdOpt match { |
| case Some(usrExtId) => |
| if (!acsUsr.isAdmin) |
| throw AdminRequired(acsUsr.email.get) |
| |
| Some(NCUserManager.getOrInsertExternalUserId(acsUsr.companyId, usrExtId)) |
| |
| case None => None |
| } |
| |
| if (id1Opt.isDefined && id2Opt.isDefined && id1Opt.get != id2Opt.get) |
| throw new InvalidArguments("User ID and external user ID are inconsistent.") |
| |
| id1Opt.getOrElse(id2Opt.getOrElse(acsUsr.id)) |
| } |
| |
| /** |
| * |
| * @param acsTkn Access token to check. |
| */ |
| @throws[NCE] |
| protected def authenticate(acsTkn: String): NCUserMdo = authenticate0(acsTkn, shouldBeAdmin = false) |
| |
| /** |
| * |
| * @param acsTkn Access token to check. |
| */ |
| @throws[NCE] |
| protected def authenticateAsAdmin(acsTkn: String): NCUserMdo = authenticate0(acsTkn, shouldBeAdmin = true) |
| |
| /** |
| * |
| * @param mdlId |
| * @param compId |
| */ |
| @throws[InvalidField] |
| protected def checkModelId(mdlId: String, compId: Long): Unit = |
| if (!NCProbeManager.existsForModel(compId, mdlId)) |
| throw InvalidModelId(mdlId) |
| |
| /** |
| * Checks length of field value. |
| * |
| * @param name Field name. |
| * @param v Field value. |
| * @param maxLen Optional maximum length. If not specified - the standard range for 'name' will be used. |
| */ |
| @throws[TooLargeField] |
| @throws[EmptyField] |
| protected def checkLength(name: String, v: String, maxLen: Int = -1): Unit = { |
| val max: Int = |
| if (maxLen == -1) |
| STD_FIELD_LENGTHS. |
| getOrElse(name, throw new AssertionError(s"Unknown standard REST field: $name")) |
| else |
| maxLen |
| |
| if (v.length > max) |
| throw TooLargeField(name, max) |
| else if (v.length < 1) |
| throw EmptyField(name) |
| } |
| |
| /** |
| * Checks length of field value. |
| * |
| * @param name Field name. |
| * @param v Field value. |
| * @param maxLen Maximum length. |
| */ |
| @throws[TooLargeField] |
| @throws[EmptyField] |
| def checkLengthOpt(name: String, v: Option[_], maxLen: Int = -1): Unit = |
| if (v.isDefined) |
| checkLength(name, v.get.toString, maxLen) |
| |
| /** |
| * |
| * @param pairs |
| */ |
| @throws[TooLargeField] |
| @throws[EmptyField] |
| protected def checkLength(pairs: (String, Any)*): Unit = |
| pairs.foreach { |
| case (name, value: Option[_]) => checkLengthOpt(name, value) |
| case (name, value: String) => checkLength(name, value) |
| |
| case _ => assert(false) |
| } |
| |
| /** |
| * Checks numeric range of field value. |
| * |
| * @param name Field name. |
| * @param v Field value. |
| * @param from Minimum from. |
| * @param to Maximum to. |
| */ |
| @throws[OutOfRangeField] |
| protected def checkRange[T](name: String, v: T, from: T, to: T)(implicit e: T => Number): Unit = { |
| val d = v.doubleValue() |
| |
| if (d < from.doubleValue() || d > to.doubleValue()) |
| throw OutOfRangeField(name, from, to) |
| } |
| |
| /** |
| * Checks numeric range of field value. |
| * |
| * @param name Field name. |
| * @param vOpt Field value. Optional. |
| * @param from Minimum from. |
| * @param to Maximum to. |
| */ |
| @throws[OutOfRangeField] |
| protected def checkRangeOpt[T](name: String, vOpt: Option[T], from: T, to: T)(implicit e: T => Number): Unit = |
| if (vOpt.isDefined) |
| checkRange(name, vOpt.get, from, to) |
| |
| /** |
| * |
| * @param js |
| * @param fn |
| */ |
| @throws[InvalidField] |
| protected def convert[T](js: JsObject, fn: String, extractor: JsValue => T): T = |
| try |
| extractor(js.fields(fn)) |
| catch { |
| case _: Exception => throw InvalidField(fn) |
| } |
| |
| /** |
| * |
| * @param js |
| * @param fn |
| * @param extractor |
| * @tparam T |
| * @return |
| */ |
| @throws[InvalidField] |
| protected def convertOpt[T](js: JsObject, fn: String, extractor: JsValue => T): Option[T] = |
| try |
| js.fields.get(fn) match { |
| case Some(v) => Some(extractor(v)) |
| case None => None |
| } |
| catch { |
| case _: Exception => throw InvalidField(fn) |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $signin(): Route = { |
| case class Req$Signin$( |
| email: String, |
| passwd: String |
| ) |
| case class Res$Signin$( |
| status: String, |
| acsTok: String |
| ) |
| implicit val reqFmt: RootJsonFormat[Req$Signin$] = jsonFormat2(Req$Signin$) |
| implicit val resFmt: RootJsonFormat[Res$Signin$] = jsonFormat2(Res$Signin$) |
| |
| // NOTE: no authentication requires on signin. |
| entity(as[Req$Signin$]) { req => |
| startScopedSpan("signin$", "email" -> req.email) { span => |
| checkLength("email" -> req.email, "passwd" -> req.passwd) |
| |
| NCUserManager.signin( |
| req.email, |
| req.passwd, |
| span |
| ) match { |
| case None => throw SignInFailure(req.email) // Email is unknown (user hasn't signed up). |
| case Some(acsTkn) => complete { |
| Res$Signin$(API_OK, acsTkn) |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $health(): Route = { |
| case class Res$Health$(status: String) |
| |
| implicit val resFmt: RootJsonFormat[Res$Health$] = jsonFormat1(Res$Health$) |
| |
| complete { |
| Res$Health$(API_OK) |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $signout(): Route = { |
| case class Req$Signout$( |
| acsTok: String |
| ) |
| case class Res$Signout$( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Signout$] = jsonFormat1(Req$Signout$) |
| implicit val resFmt: RootJsonFormat[Res$Signout$] = jsonFormat1(Res$Signout$) |
| |
| entity(as[Req$Signout$]) { req => |
| startScopedSpan("signout$", "acsTok" -> req.acsTok) { span => |
| checkLength("acsTok" -> req.acsTok) |
| |
| val usr = authenticate(req.acsTok) |
| |
| for (mdlId <- NCProbeManager.getAllProbes(usr.companyId, span).flatMap(_.models.map(_.id))) { |
| NCProbeManager.clearConversation(usr.id, mdlId, span) |
| NCProbeManager.clearDialog(usr.id, mdlId, span) |
| } |
| |
| NCUserManager.signout(req.acsTok, span) |
| |
| complete { |
| Res$Signout$(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @param fut |
| */ |
| private def successWithJs(fut: Future[String]): Route = |
| onSuccess(fut) { |
| js => complete(HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, js))) |
| } |
| |
| /** |
| * |
| * @param o |
| */ |
| private def completeJs(o: Object): Route = |
| complete(HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, toJs(o)))) |
| |
| /** |
| * |
| * @param gzipOpt |
| */ |
| @throws[NCE] |
| private def unzipProperties(gzipOpt: Option[String]): Option[JavaMeta] = |
| gzipOpt match { |
| case Some(gzip) => Some(jsonToJavaMap(uncompress(gzip))) |
| case None => None |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| private def ask0(process: AskReqHolder => Route): Route = { |
| case class Req$Ask$( |
| acsTok: String, |
| usrId: Option[Long], |
| usrExtId: Option[String], |
| txt: String, |
| mdlId: String, |
| data: Option[spray.json.JsValue], |
| enableLog: Option[Boolean] |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Ask$] = jsonFormat7(Req$Ask$) |
| |
| entity(as[Req$Ask$]) { req => |
| startScopedSpan( |
| "ask$", |
| "usrId" -> req.usrId.getOrElse(-1), |
| "usrExtId" -> req.usrExtId.orNull, |
| "acsTok" -> req.acsTok, |
| "txt" -> req.txt, |
| "mdlId" -> req.mdlId) { span => |
| checkLength( |
| "acsTok" -> req.acsTok, "usrExtId" -> req.usrExtId, "mdlId" -> req.mdlId, "txt" -> req.txt |
| ) |
| |
| val dataJs = extractJson(req.data, "data") |
| val acsUsr = authenticate(req.acsTok) |
| |
| checkModelId(req.mdlId, acsUsr.companyId) |
| |
| optionalHeaderValueByName("User-Agent") { usrAgent => |
| extractClientIP { rmtAddr => |
| process(AskReqHolder( |
| usrId = getUserId(acsUsr, req.usrId, req.usrExtId), |
| txt = req.txt, |
| mdlId = req.mdlId, |
| usrAgent = usrAgent, |
| rmtAddr = getAddress(rmtAddr), |
| data = dataJs, |
| req.enableLog.getOrElse(false), |
| parent = span |
| )) |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $ask(): Route = |
| ask0( |
| (h: AskReqHolder) => { |
| val newSrvReqId = NCQueryManager.asyncAsk( |
| h.usrId, |
| h.txt, |
| h.mdlId, |
| h.usrAgent, |
| h.rmtAddr, |
| h.data, |
| h.enableLog, |
| h.parent |
| ) |
| |
| case class Res$Ask$( |
| status: String, |
| srvReqId: String |
| ) |
| |
| implicit val resFmt: RootJsonFormat[Res$Ask$] = jsonFormat2(Res$Ask$) |
| |
| complete { |
| Res$Ask$(API_OK, newSrvReqId) |
| } |
| } |
| ) |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $ask$Sync(): Route = |
| ask0( |
| (h: AskReqHolder) => { |
| val fut = NCQueryManager.futureAsk( |
| h.usrId, |
| h.txt, |
| h.mdlId, |
| h.usrAgent, |
| h.rmtAddr, |
| h.data, |
| h.enableLog, |
| h.parent |
| ) |
| |
| successWithJs( |
| fut.collect { |
| // We have to use Jackson (not spray) here to serialize 'resBody' field. |
| case res => toJs( |
| Map( |
| "status" -> API_OK.toString, |
| "state" -> queryStateToMap(res) |
| ).asJava |
| ) |
| } |
| ) |
| } |
| ) |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $cancel(): Route = { |
| case class Req$Cancel$( |
| acsTok: String, |
| usrId: Option[Long], |
| usrExtId: Option[String], |
| srvReqIds: Option[Set[String]] |
| ) |
| case class Res$Cancel$( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Cancel$] = jsonFormat4(Req$Cancel$) |
| implicit val resFmt: RootJsonFormat[Res$Cancel$] = jsonFormat1(Res$Cancel$) |
| |
| entity(as[Req$Cancel$]) { req => |
| startScopedSpan("cancel$", |
| "acsTok" -> req.acsTok, |
| "usrId" -> req.usrId.getOrElse(-1), |
| "usrExtId" -> req.usrExtId.orNull, |
| "srvReqIds" -> req.srvReqIds.getOrElse(Nil).mkString(",")) { span => |
| checkLength("acsTok" -> req.acsTok, "usrExtId" -> req.usrExtId) |
| |
| val acsUsr = authenticate(req.acsTok) |
| |
| val srvReqs = getRequests(acsUsr, req.srvReqIds, req.usrId, req.usrExtId, span) |
| |
| NCQueryManager.cancelForServerRequestIds(srvReqs.map(_.srvReqId), span) |
| |
| complete { |
| Res$Cancel$(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $check(): Route = { |
| case class Req$Check$( |
| acsTok: String, |
| usrId: Option[Long], |
| usrExtId: Option[String], |
| srvReqIds: Option[Set[String]], |
| maxRows: Option[Int] |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Check$] = jsonFormat5(Req$Check$) |
| |
| entity(as[Req$Check$]) { req => |
| startScopedSpan( |
| "check$", |
| "usrId" -> req.usrId.getOrElse(-1), |
| "usrExtId" -> req.usrExtId.orNull, |
| "acsTok" -> req.acsTok, |
| "srvReqIds" -> req.srvReqIds.getOrElse(Nil).mkString(",") |
| ) { span => |
| checkLength("acsTok" -> req.acsTok, "usrExtId" -> req.usrExtId) |
| checkRangeOpt("maxRows", req.maxRows, 1, java.lang.Integer.MAX_VALUE) |
| |
| val acsUsr = authenticate(req.acsTok) |
| |
| val states = |
| getRequests(acsUsr, req.srvReqIds, req.usrId, req.usrExtId, span). |
| toSeq.sortBy(-_.createTstamp.getTime). |
| take(req.maxRows.getOrElse(Integer.MAX_VALUE)) |
| |
| // We have to use Jackson (not spray) here to serialize 'resBody' field. |
| val js = toJs( |
| Map( |
| "status" -> API_OK.toString, |
| "states" -> states.map(queryStateToMap).asJava |
| ) |
| .asJava |
| ) |
| |
| complete( |
| HttpResponse( |
| entity = HttpEntity(ContentTypes.`application/json`, js) |
| ) |
| ) |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $model$sugsyn(): Route = { |
| case class Req$Model$Sugsyn( |
| acsTok: String, |
| mdlId: String, |
| minScore: Option[Double] |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Model$Sugsyn] = jsonFormat3(Req$Model$Sugsyn) |
| |
| entity(as[Req$Model$Sugsyn]) { req => |
| startScopedSpan( |
| "model$sugsyn", |
| "acsTok" -> req.acsTok, |
| "mdlId" -> req.mdlId, |
| "minScore" -> req.minScore.getOrElse(-1)) { span => |
| checkLength("acsTok" -> req.acsTok, "mdlId" -> req.mdlId) |
| checkRangeOpt("minScore", req.minScore, 0.0, 1.0) |
| |
| val admUsr = authenticateAsAdmin(req.acsTok) |
| |
| checkModelId(req.mdlId, admUsr.companyId) |
| |
| val fut = NCSuggestSynonymManager.suggest(req.mdlId, req.minScore, span) |
| |
| successWithJs( |
| fut.collect { |
| // We have to use Jackson (not spray) here to serialize 'result' field. |
| case res => toJs(Map("status" -> API_OK.toString, "result" -> res).asJava) |
| } |
| ) |
| } |
| } |
| } |
| |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $model$syns(): Route = { |
| case class Req$Model$Syns( |
| acsTok: String, |
| mdlId: String, |
| elmId: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Model$Syns] = jsonFormat3(Req$Model$Syns) |
| |
| entity(as[Req$Model$Syns]) { req => |
| startScopedSpan( |
| "model$syns", |
| "acsTok" -> req.acsTok, |
| "mdlId" -> req.mdlId, |
| "elmId" -> req.elmId) { span => |
| checkLength("acsTok" -> req.acsTok, "mdlId" -> req.mdlId, "elmId" -> req.elmId) |
| |
| val admUsr = authenticateAsAdmin(req.acsTok) |
| val compId = admUsr.companyId |
| |
| if (!NCProbeManager.existsForModel(compId, req.mdlId)) |
| throw InvalidModelId(req.mdlId) |
| if (!NCProbeManager.existsForModelElement(compId, req.mdlId, req.elmId)) |
| throw InvalidModelOrElementId(req.mdlId, req.elmId) |
| |
| val fut = NCProbeManager.getElementInfo(req.mdlId, req.elmId, span) |
| |
| successWithJs( |
| fut.collect { |
| // We have to use Jackson (not spray) here to serialize 'result' field. |
| case res => |
| require(res.containsKey("synonyms") && res.containsKey("values")) |
| |
| toJs( |
| Map( |
| "status" -> API_OK.toString, |
| "synonyms" -> res.get("synonyms"), |
| "values" -> res.get("values") |
| ) |
| ) |
| } |
| ) |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $clear$Conversation(): Route = { |
| case class Req$Clear$Conversation( |
| acsTok: String, |
| mdlId: String, |
| usrId: Option[Long], |
| usrExtId: Option[String] |
| ) |
| case class Res$Clear$Conversation( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Clear$Conversation] = jsonFormat4(Req$Clear$Conversation) |
| implicit val resFmt: RootJsonFormat[Res$Clear$Conversation] = jsonFormat1(Res$Clear$Conversation) |
| |
| entity(as[Req$Clear$Conversation]) { req => |
| startScopedSpan( |
| "clear$Conversation", |
| "acsTok" -> req.acsTok, |
| "mdlId" -> req.mdlId, |
| "usrExtId" -> req.usrExtId.orNull, |
| "usrId" -> req.usrId.getOrElse(-1)) { span => |
| //noinspection DuplicatedCode |
| checkLength( |
| "acsTok" -> req.acsTok, |
| "mdlId" -> req.mdlId, |
| "usrExtId" -> req.usrExtId |
| ) |
| |
| val acsUsr = authenticate(req.acsTok) |
| |
| checkModelId(req.mdlId, acsUsr.companyId) |
| |
| NCProbeManager.clearConversation(getUserId(acsUsr, req.usrId, req.usrExtId), req.mdlId, span) |
| |
| complete { |
| Res$Clear$Conversation(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $clear$Dialog(): Route = { |
| case class Req$Clear$Dialog( |
| acsTok: String, |
| mdlId: String, |
| usrId: Option[Long], |
| usrExtId: Option[String] |
| ) |
| case class Res$Clear$Dialog( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Clear$Dialog] = jsonFormat4(Req$Clear$Dialog) |
| implicit val resFmt: RootJsonFormat[Res$Clear$Dialog] = jsonFormat1(Res$Clear$Dialog) |
| |
| entity(as[Req$Clear$Dialog]) { req => |
| startScopedSpan( |
| "clear$Dialog", |
| "acsTok" -> req.acsTok, |
| "usrExtId" -> req.usrExtId.orNull, |
| "mdlId" -> req.mdlId, |
| "usrId" -> req.usrId.getOrElse(-1)) { span => |
| //noinspection DuplicatedCode |
| checkLength( |
| "acsTok" -> req.acsTok, |
| "mdlId" -> req.mdlId, |
| "usrExtId" -> req.usrExtId |
| ) |
| |
| val acsUsr = authenticate(req.acsTok) |
| |
| checkModelId(req.mdlId, acsUsr.companyId) |
| |
| NCProbeManager.clearDialog(getUserId(acsUsr, req.usrId, req.usrExtId), req.mdlId, span) |
| |
| complete { |
| Res$Clear$Dialog(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $company$Add(): Route = { |
| case class Req$Company$Add( |
| acsTok: String, |
| // New company. |
| name: String, |
| website: Option[String], |
| country: Option[String], |
| region: Option[String], |
| city: Option[String], |
| address: Option[String], |
| postalCode: Option[String], |
| // New company admin. |
| adminEmail: String, |
| adminPasswd: String, |
| adminFirstName: String, |
| adminLastName: String, |
| adminAvatarUrl: Option[String], |
| properties: Option[spray.json.JsValue] |
| ) |
| case class Res$Company$Add( |
| status: String, |
| token: String, |
| adminId: Long |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Company$Add] = jsonFormat14(Req$Company$Add) |
| implicit val resFmt: RootJsonFormat[Res$Company$Add] = jsonFormat3(Res$Company$Add) |
| |
| //noinspection DuplicatedCod |
| entity(as[Req$Company$Add]) { req => |
| startScopedSpan("company$Add", "name" -> req.name) { span => |
| checkLength( |
| "acsTok" -> req.acsTok, |
| "name" -> req.name, |
| "website" -> req.website, |
| "country" -> req.country, |
| "region" -> req.region, |
| "city" -> req.city, |
| "address" -> req.address, |
| "postalCode" -> req.postalCode, |
| "adminEmail" -> req.adminEmail, |
| "adminPasswd" -> req.adminPasswd, |
| "adminFirstName" -> req.adminFirstName, |
| "adminLastName" -> req.adminLastName, |
| "adminAvatarUrl" -> req.adminAvatarUrl |
| ) |
| |
| val propsJs = extractJson(req.properties, "properties") |
| |
| // Via REST only administrators of already created companies can create new companies. |
| authenticateAsAdmin(req.acsTok) |
| |
| val res = NCCompanyManager.addCompany( |
| req.name, |
| req.website, |
| req.country, |
| req.region, |
| req.city, |
| req.address, |
| req.postalCode, |
| req.adminEmail, |
| req.adminPasswd, |
| req.adminFirstName, |
| req.adminLastName, |
| req.adminAvatarUrl, |
| propsJs, |
| span |
| ) |
| |
| complete { |
| Res$Company$Add(API_OK, res.token, res.adminId) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $company$Get(): Route = { |
| case class Req$Company$Get( |
| acsTok: String |
| ) |
| case class Res$Company$Get( |
| status: String, |
| id: Long, |
| name: String, |
| website: Option[String], |
| country: Option[String], |
| region: Option[String], |
| city: Option[String], |
| address: Option[String], |
| postalCode: Option[String], |
| properties: Option[JavaMeta] |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Company$Get] = jsonFormat1(Req$Company$Get) |
| |
| entity(as[Req$Company$Get]) { req => |
| startScopedSpan("company$get", "acsTok" -> req.acsTok) { span => |
| checkLength("acsTok" -> req.acsTok) |
| |
| val acsUsr = authenticate(req.acsTok) |
| |
| val company = NCCompanyManager. |
| getCompany(acsUsr.companyId, span). |
| getOrElse(throw new NCE(s"Company not found: ${acsUsr.companyId}")) |
| |
| completeJs { |
| Res$Company$Get(API_OK, |
| company.id, |
| company.name, |
| company.website, |
| company.country, |
| company.region, |
| company.city, |
| company.address, |
| company.postalCode, |
| unzipProperties(company.propertiesGzip) |
| ) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $company$Update(): Route = { |
| case class Req$Company$Update( |
| // Caller. |
| acsTok: String, |
| |
| // Updated company. |
| name: String, |
| website: Option[String], |
| country: Option[String], |
| region: Option[String], |
| city: Option[String], |
| address: Option[String], |
| postalCode: Option[String], |
| properties: Option[spray.json.JsValue] |
| ) |
| case class Res$Company$Update( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Company$Update] = jsonFormat9(Req$Company$Update) |
| implicit val resFmt: RootJsonFormat[Res$Company$Update] = jsonFormat1(Res$Company$Update) |
| |
| entity(as[Req$Company$Update]) { req => |
| startScopedSpan("company$Update", "acsTok" -> req.acsTok, "name" -> req.name) { span => |
| checkLength("acsTok" -> req.acsTok, |
| "name" -> req.name, |
| "website" -> req.website, |
| "country" -> req.country, |
| "region" -> req.region, |
| "city" -> req.city, |
| "address" -> req.address, |
| "postalCode" -> req.postalCode |
| ) |
| |
| val propsJs = extractJson(req.properties, "properties") |
| val admUsr = authenticateAsAdmin(req.acsTok) |
| |
| NCCompanyManager.updateCompany( |
| admUsr.companyId, |
| req.name, |
| req.website, |
| req.country, |
| req.region, |
| req.city, |
| req.address, |
| req.postalCode, |
| propsJs, |
| span |
| ) |
| |
| complete { |
| Res$Company$Update(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $feedback$Add(): Route = { |
| case class Req$Feedback$Add( |
| acsTok: String, |
| usrId : Option[Long], |
| usrExtId: Option[String], |
| srvReqId: String, |
| score: Double, |
| comment: Option[String] |
| ) |
| case class Res$Feedback$Add( |
| status: String, |
| id: Long |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Feedback$Add] = jsonFormat6(Req$Feedback$Add) |
| implicit val resFmt: RootJsonFormat[Res$Feedback$Add] = jsonFormat2(Res$Feedback$Add) |
| |
| entity(as[Req$Feedback$Add]) { req => |
| startScopedSpan( |
| "feedback$Add", |
| "usrId" -> req.usrId.getOrElse(-1), |
| "usrExtId" -> req.usrExtId.orNull, |
| "srvReqId" -> req.srvReqId) { span => |
| checkLength( |
| "acsTok" -> req.acsTok, |
| "usrExtId" -> req.usrExtId, |
| "srvReqId" -> req.srvReqId, |
| "comment" -> req.comment |
| ) |
| checkRange("score", req.score, 0, 1) |
| |
| // Via REST only administrators of already created companies can create new companies. |
| val acsUsr = authenticate(req.acsTok) |
| |
| val id = NCFeedbackManager.addFeedback( |
| req.srvReqId, |
| getUserId(acsUsr, req.usrId, req.usrExtId), |
| req.score, |
| req.comment, |
| span |
| ) |
| |
| complete { |
| Res$Feedback$Add(API_OK, id) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $feedback$Delete(): Route = { |
| case class Req$Feedback$Delete( |
| acsTok: String, |
| // Feedback IDs to delete (optional). |
| id: Option[Long] |
| ) |
| case class Res$Feedback$Delete( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Feedback$Delete] = jsonFormat2(Req$Feedback$Delete) |
| implicit val resFmt: RootJsonFormat[Res$Feedback$Delete] = jsonFormat1(Res$Feedback$Delete) |
| |
| entity(as[Req$Feedback$Delete]) { req => |
| startScopedSpan("feedback$Delete") { span => |
| checkLength("acsTok" -> req.acsTok) |
| |
| // Via REST only administrators of already created companies can create new companies. |
| val acsUsr = authenticate(req.acsTok) |
| |
| require(acsUsr.email.isDefined) |
| |
| req.id match { |
| case Some(id) => |
| NCFeedbackManager.getFeedback(id, span) match { |
| case Some(f) => |
| val companyId = |
| NCUserManager. |
| getUserById(f.userId, span). |
| getOrElse(throw new NCE(s"Company not found for user: ${f.userId}")). |
| companyId |
| |
| if (companyId != acsUsr.companyId || (f.userId != acsUsr.id && !acsUsr.isAdmin)) |
| throw AdminRequired(acsUsr.email.get) |
| |
| NCFeedbackManager.deleteFeedback(f.id, span) |
| case None => // No-op. |
| } |
| |
| case None => |
| if (!acsUsr.isAdmin) |
| throw AdminRequired(acsUsr.email.get) |
| |
| NCFeedbackManager.deleteAllFeedback(acsUsr.companyId, span) |
| } |
| |
| complete { |
| Res$Feedback$Delete(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $feedback$All(): Route = { |
| case class Req$Feedback$All( |
| acsTok: String, |
| usrId: Option[Long], |
| usrExtId: Option[String], |
| srvReqId: Option[String] |
| ) |
| case class Feedback_Feedback$All( |
| id: Long, |
| srvReqId: String, |
| usrId: Long, |
| score: Double, |
| comment: Option[String], |
| createTstamp: Long |
| ) |
| case class Res$Feedback$All( |
| status: String, |
| feedback: Seq[Feedback_Feedback$All] |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Feedback$All] = jsonFormat4(Req$Feedback$All) |
| implicit val fbFmt: RootJsonFormat[Feedback_Feedback$All] = jsonFormat6(Feedback_Feedback$All) |
| implicit val resFmt: RootJsonFormat[Res$Feedback$All] = jsonFormat2(Res$Feedback$All) |
| |
| entity(as[Req$Feedback$All]) { req => |
| startScopedSpan( |
| "feedback$All", |
| "usrId" -> req.usrId.getOrElse(-1), |
| "usrExtId" -> req.usrExtId.orNull |
| ) { span => |
| checkLength("acsTok" -> req.acsTok, "srvReqId" -> req.srvReqId, "usrExtId" -> req.usrExtId) |
| |
| val acsUsr = authenticate(req.acsTok) |
| |
| require(acsUsr.email.isDefined) |
| |
| val feedback = |
| NCFeedbackManager.getFeedback( |
| acsUsr.companyId, |
| req.srvReqId, |
| if (req.usrId.isDefined || req.usrExtId.isDefined) |
| Some(getUserId(acsUsr, req.usrId, req.usrExtId)) |
| else |
| None, |
| span |
| ).map(f => |
| Feedback_Feedback$All( |
| f.id, |
| f.srvReqId, |
| f.userId, |
| f.score, |
| f.feedbackComment, |
| f.createdOn.getTime |
| ) |
| ) |
| |
| if (!acsUsr.isAdmin && feedback.exists(_.usrId != acsUsr.id)) |
| throw AdminRequired(acsUsr.email.get) |
| |
| complete { |
| Res$Feedback$All(API_OK, feedback) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $company$Token$Reset(): Route = { |
| case class Req$Company$Token$Reset( |
| // Caller. |
| acsTok: String |
| ) |
| case class Res$Company$Token$Reset( |
| status: String, |
| token: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Company$Token$Reset] = jsonFormat1(Req$Company$Token$Reset) |
| implicit val resFmt: RootJsonFormat[Res$Company$Token$Reset] = jsonFormat2(Res$Company$Token$Reset) |
| |
| entity(as[Req$Company$Token$Reset]) { req => |
| startScopedSpan("company$Token$Reset", "acsTok" -> req.acsTok) { span => |
| checkLength("acsTok" -> req.acsTok) |
| |
| val admUsr = authenticateAsAdmin(req.acsTok) |
| |
| val tkn = NCCompanyManager.resetToken(admUsr.companyId, span) |
| |
| complete { |
| Res$Company$Token$Reset(API_OK, tkn) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $company$Delete(): Route = { |
| case class Req$Company$Delete( |
| // Caller. |
| acsTok: String |
| ) |
| case class Res$Company$Delete( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Company$Delete] = jsonFormat1(Req$Company$Delete) |
| implicit val resFmt: RootJsonFormat[Res$Company$Delete] = jsonFormat1(Res$Company$Delete) |
| |
| entity(as[Req$Company$Delete]) { req => |
| startScopedSpan("company$Delete", "acsTok" -> req.acsTok) { span => |
| checkLength("acsTok" -> req.acsTok) |
| |
| val admUSr = authenticateAsAdmin(req.acsTok) |
| |
| NCCompanyManager.deleteCompany(admUSr.companyId, span) |
| |
| complete { |
| Res$Company$Delete(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $user$Add(): Route = { |
| case class Req$User$Add( |
| // Caller. |
| acsTok: String, |
| |
| // New user. |
| email: String, |
| passwd: String, |
| firstName: String, |
| lastName: String, |
| avatarUrl: Option[String], |
| isAdmin: Boolean, |
| properties: Option[spray.json.JsValue], |
| usrExtId: Option[String] |
| ) |
| case class Res$User$Add( |
| status: String, |
| id: Long |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$User$Add] = jsonFormat9(Req$User$Add) |
| implicit val resFmt: RootJsonFormat[Res$User$Add] = jsonFormat2(Res$User$Add) |
| |
| entity(as[Req$User$Add]) { req => |
| startScopedSpan("user$Add", "acsTok" -> req.acsTok, "email" -> req.email) { span => |
| checkLength( |
| "acsTok" -> req.acsTok, |
| "email" -> req.email, |
| "passwd" -> req.passwd, |
| "firstName" -> req.firstName, |
| "lastName" -> req.lastName, |
| "avatarUrl" -> req.avatarUrl, |
| "usrExtId" -> req.usrExtId |
| ) |
| |
| val propsJs = extractJson(req.properties, "properties") |
| val admUsr = authenticateAsAdmin(req.acsTok) |
| |
| val id = NCUserManager.addUser( |
| admUsr.companyId, |
| req.email, |
| req.passwd, |
| req.firstName, |
| req.lastName, |
| req.avatarUrl, |
| req.isAdmin, |
| propsJs, |
| req.usrExtId, |
| span |
| ) |
| |
| complete { |
| Res$User$Add(API_OK, id) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $user$Update(): Route = { |
| case class Req$User$Update( |
| // Caller. |
| acsTok: String, |
| |
| // Update user. |
| id: Option[Long], |
| firstName: String, |
| lastName: String, |
| avatarUrl: Option[String], |
| properties: Option[spray.json.JsValue] |
| ) |
| case class Res$User$Update( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$User$Update] = jsonFormat6(Req$User$Update) |
| implicit val resFmt: RootJsonFormat[Res$User$Update] = jsonFormat1(Res$User$Update) |
| |
| entity(as[Req$User$Update]) { req => |
| startScopedSpan("user$Update", "acsTok" -> req.acsTok, "usrId" -> req.id.getOrElse(-1)) { span => |
| checkLength( |
| "acsTok" -> req.acsTok, |
| "firstName" -> req.firstName, |
| "lastName" -> req.lastName, |
| "avatarUrl" -> req.avatarUrl |
| ) |
| |
| val propsJs = extractJson(req.properties, "properties") |
| val acsUsr = authenticate(req.acsTok) |
| |
| NCUserManager.updateUser( |
| getUserId(acsUsr, req.id, None), |
| req.firstName, |
| req.lastName, |
| req.avatarUrl, |
| propsJs, |
| span |
| ) |
| |
| complete { |
| Res$User$Update(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $user$Delete(): Route = { |
| case class Req$User$Delete( |
| acsTok: String, |
| id: Option[Long], |
| usrExtId: Option[String] |
| ) |
| case class Res$User$Delete( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$User$Delete] = jsonFormat3(Req$User$Delete) |
| implicit val resFmt: RootJsonFormat[Res$User$Delete] = jsonFormat1(Res$User$Delete) |
| |
| entity(as[Req$User$Delete]) { req => |
| startScopedSpan( |
| "user$Delete", "acsTok" -> req.acsTok, "usrId" -> req.id.getOrElse(-1) |
| ) { span => |
| checkLength("acsTok" -> req.acsTok, "usrExtId" -> req.usrExtId) |
| |
| val acsUsr = authenticate(req.acsTok) |
| |
| require(acsUsr.email.isDefined) |
| |
| def delete(id: Long): Unit = { |
| NCUserManager.signoutAllSessions(id, span) |
| NCUserManager.deleteUser(id, span) |
| |
| logger.info(s"User deleted: $id") |
| } |
| |
| // Deletes all users from company except initiator. |
| if (req.id.isEmpty && req.usrExtId.isEmpty) { |
| if (!acsUsr.isAdmin) |
| throw AdminRequired(acsUsr.email.get) |
| |
| NCUserManager. |
| getAllUsers(acsUsr.companyId, span). |
| filter(_.id != acsUsr.id). |
| map(_.id). |
| foreach(delete) |
| } |
| else { |
| val delUsrId = getUserId(acsUsr, req.id, req.usrExtId) |
| |
| // Tries to delete own account. |
| if (delUsrId == acsUsr.id && |
| acsUsr.isAdmin && |
| !NCUserManager.isOtherAdminsExist(acsUsr.id) |
| ) |
| throw InvalidOperation(s"Last admin user cannot be deleted: ${acsUsr.email.get}") |
| |
| delete(delUsrId) |
| } |
| |
| complete { |
| Res$User$Delete(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $user$Admin(): Route = { |
| case class Req$User$Admin( |
| acsTok: String, |
| id: Option[Long], |
| admin: Boolean |
| ) |
| case class Res$User$Admin( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$User$Admin] = jsonFormat3(Req$User$Admin) |
| implicit val resFmt: RootJsonFormat[Res$User$Admin] = jsonFormat1(Res$User$Admin) |
| |
| entity(as[Req$User$Admin]) { req => |
| startScopedSpan( |
| "user$Admin", "acsTok" -> req.acsTok, "usrId" -> req.id.getOrElse(-1), "admin" -> req.admin |
| ) { span => |
| checkLength("acsTok" -> req.acsTok) |
| |
| val initUsr = authenticateAsAdmin(req.acsTok) |
| val usrId = req.id.getOrElse(initUsr.id) |
| |
| // Self update. |
| if ( |
| usrId == initUsr.id && |
| !req.admin && |
| !NCUserManager.isOtherAdminsExist(initUsr.id, span) |
| ) |
| throw InvalidOperation(s"Last admin user cannot lose admin privileges: ${initUsr.email}") |
| |
| NCUserManager.updateUserPermissions(usrId, req.admin, span) |
| |
| complete { |
| Res$User$Admin(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $user$Passwd$Reset(): Route = { |
| case class Req$User$Password$Reset( |
| // Caller. |
| acsTok: String, |
| id: Option[Long], |
| newPasswd: String |
| ) |
| case class Res$User$Password$Reset( |
| status: String |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$User$Password$Reset] = jsonFormat3(Req$User$Password$Reset) |
| implicit val resFmt: RootJsonFormat[Res$User$Password$Reset] = jsonFormat1(Res$User$Password$Reset) |
| |
| entity(as[Req$User$Password$Reset]) { req => |
| startScopedSpan( |
| "user$Password$Reset", |
| "acsTok" -> req.acsTok, "usrId" -> req.id.getOrElse(-1)) { span => |
| checkLength("acsTok" -> req.acsTok, "newPasswd" -> req.newPasswd) |
| |
| val acsUsr = authenticate(req.acsTok) |
| |
| NCUserManager.resetPassword(getUserId(acsUsr, req.id, None), req.newPasswd, span) |
| |
| complete { |
| Res$User$Password$Reset(API_OK) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $user$All(): Route = { |
| case class Req$User$All( |
| // Caller. |
| acsTok: String |
| ) |
| case class ResUser_User$All( |
| id: Long, |
| email: Option[String], |
| usrExtId: Option[String], |
| firstName: Option[String], |
| lastName: Option[String], |
| avatarUrl: Option[String], |
| isAdmin: Boolean, |
| companyId: Long, |
| properties: Option[JavaMeta] |
| ) |
| case class Res$User$All( |
| status: String, |
| users: Seq[ResUser_User$All] |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$User$All] = jsonFormat1(Req$User$All) |
| |
| entity(as[Req$User$All]) { req => |
| startScopedSpan("user$All", "acsTok" -> req.acsTok) { span => |
| checkLength("acsTok" -> req.acsTok) |
| |
| val admUSr = authenticateAsAdmin(req.acsTok) |
| |
| completeJs { |
| Res$User$All( |
| API_OK, |
| NCUserManager.getAllUsers(admUSr.companyId, span).map(u => |
| ResUser_User$All( |
| u.id, |
| u.email, |
| u.extId, |
| u.firstName, |
| u.lastName, |
| u.avatarUrl, |
| u.isAdmin, |
| u.companyId, |
| unzipProperties(u.propertiesGzip) |
| ) |
| ) |
| ) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $user$Get(): Route = { |
| case class Req$User$Get( |
| // Caller. |
| acsTok: String, |
| id: Option[Long], |
| usrExtId: Option[String] |
| ) |
| case class Res$User$Get( |
| status: String, |
| id: Long, |
| email: Option[String], |
| usrExtId: Option[String], |
| firstName: Option[String], |
| lastName: Option[String], |
| avatarUrl: Option[String], |
| isAdmin: Boolean, |
| properties: Option[JavaMeta] |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$User$Get] = jsonFormat3(Req$User$Get) |
| |
| entity(as[Req$User$Get]) { req => |
| startScopedSpan( |
| "user$Get", "acsTok" -> req.acsTok, "id" -> req.id.orElse(null), "usrExtId" -> req.usrExtId.orNull |
| ) { span => |
| checkLength("acsTok" -> req.acsTok, "usrExtId" -> req.usrExtId) |
| |
| val acsUsr = authenticate(req.acsTok) |
| val usrId = getUserId(acsUsr, req.id, req.usrExtId) |
| |
| if (acsUsr.id != usrId && !acsUsr.isAdmin) |
| throw AdminRequired(acsUsr.email.get) |
| |
| val usr = NCUserManager.getUserById(usrId, span).getOrElse(throw new NCE(s"User not found: $usrId")) |
| |
| completeJs { |
| Res$User$Get(API_OK, |
| usr.id, |
| usr.email, |
| usr.extId, |
| usr.firstName, |
| usr.lastName, |
| usr.avatarUrl, |
| usr.isAdmin, |
| unzipProperties(usr.propertiesGzip) |
| ) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| protected def $probe$All(): Route = { |
| case class Req$Probe$All( |
| acsTok: String |
| ) |
| case class Model_Probe$All( |
| id: String, |
| name: String, |
| version: String, |
| enabledBuiltInTokens: Set[String] |
| ) |
| case class Probe_Probe$All( |
| probeToken: String, |
| probeId: String, |
| probeGuid: String, |
| probeApiVersion: String, |
| probeApiDate: String, |
| osVersion: String, |
| osName: String, |
| osArch: String, |
| startTstamp: Long, |
| tmzId: String, |
| tmzAbbr: String, |
| tmzName: String, |
| userName: String, |
| javaVersion: String, |
| javaVendor: String, |
| hostName: String, |
| hostAddr: String, |
| macAddr: String, |
| models: Set[Model_Probe$All] |
| ) |
| case class Res$Probe$All( |
| status: String, |
| probes: Seq[Probe_Probe$All] |
| ) |
| |
| implicit val reqFmt: RootJsonFormat[Req$Probe$All] = jsonFormat1(Req$Probe$All) |
| implicit val mdlFmt: RootJsonFormat[Model_Probe$All] = jsonFormat4(Model_Probe$All) |
| implicit val probFmt: RootJsonFormat[Probe_Probe$All] = jsonFormat19(Probe_Probe$All) |
| implicit val resFmt: RootJsonFormat[Res$Probe$All] = jsonFormat2(Res$Probe$All) |
| |
| entity(as[Req$Probe$All]) { req => |
| startScopedSpan("probe$All", "acsTok" -> req.acsTok) { span => |
| checkLength("acsTok" -> req.acsTok) |
| |
| val admUsr = authenticateAsAdmin(req.acsTok) |
| |
| val probeLst = NCProbeManager.getAllProbes(admUsr.companyId, span).map(mdo => Probe_Probe$All( |
| mdo.probeToken, |
| mdo.probeId, |
| mdo.probeGuid, |
| mdo.probeApiVersion, |
| mdo.probeApiDate.toString, |
| mdo.osVersion, |
| mdo.osName, |
| mdo.osArch, |
| mdo.startTstamp.getTime, |
| mdo.tmzId, |
| mdo.tmzAbbr, |
| mdo.tmzName, |
| mdo.userName, |
| mdo.javaVersion, |
| mdo.javaVendor, |
| mdo.hostName, |
| mdo.hostAddr, |
| mdo.macAddr, |
| mdo.models.map(m => Model_Probe$All( |
| m.id, |
| m.name, |
| m.version, |
| m.enabledBuiltInTokens |
| )) |
| )) |
| |
| complete { |
| Res$Probe$All(API_OK, probeLst) |
| } |
| } |
| } |
| } |
| |
| /** |
| * |
| * @param statusCode |
| * @param errCode |
| * @param errMsg |
| */ |
| protected def completeError(statusCode: StatusCode, errCode: String, errMsg: String): Route = { |
| currentSpan().setStatus(Status.INTERNAL.withDescription(s"code: $errCode, message: $errMsg")) |
| |
| corsHandler( |
| complete( |
| HttpResponse( |
| status = statusCode, |
| entity = HttpEntity( |
| ContentTypes.`application/json`, |
| toJs(Map("code" -> errCode, "msg" -> errMsg).asJava) |
| ) |
| ) |
| ) |
| ) |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| def getExceptionHandler: ExceptionHandler = ExceptionHandler { |
| case e: AccessTokenFailure => |
| val errMsg = e.getLocalizedMessage |
| val code = "NC_INVALID_ACCESS_TOKEN" |
| |
| completeError(StatusCodes.Unauthorized, code, errMsg) |
| |
| case e: SignInFailure => |
| val errMsg = e.getLocalizedMessage |
| val code = "NC_SIGNIN_FAILURE" |
| |
| completeError(StatusCodes.Unauthorized, code, errMsg) |
| |
| case e: NotImplemented => |
| val errMsg = e.getLocalizedMessage |
| val code = "NC_NOT_IMPLEMENTED" |
| |
| completeError(StatusCodes.NotImplemented, code, errMsg) |
| |
| case e: InvalidArguments => |
| val errMsg = e.getLocalizedMessage |
| val code = "NC_INVALID_FIELD" |
| |
| completeError(StatusCodes.BadRequest, code, errMsg) |
| |
| case e: AdminRequired => |
| val errMsg = e.getLocalizedMessage |
| val code = "NC_ADMIN_REQUIRED" |
| |
| completeError(StatusCodes.Forbidden, code, errMsg) |
| |
| case e: InvalidOperation => |
| val errMsg = e.getLocalizedMessage |
| val code = "NC_INVALID_OPERATION" |
| |
| completeError(StatusCodes.Forbidden, code, errMsg) |
| |
| // General exception. |
| case e: NCE => |
| val errMsg = "Unexpected system error." |
| val code = "NC_ERROR" |
| |
| // We have to log error reason because even general exceptions are not expected here. |
| U.prettyError(logger,s"Unexpected top level REST API error: $errMsg", e) |
| |
| completeError(StatusCodes.BadRequest, code, errMsg) |
| |
| // Unexpected errors. |
| case e: Throwable => |
| val errMsg = "Unexpected system error." |
| val code = "NC_ERROR" |
| |
| U.prettyError(logger,s"Unexpected system error: $errMsg", e) |
| |
| completeError(StatusCodes.InternalServerError, code, errMsg) |
| } |
| |
| /** |
| * |
| * @return |
| */ |
| def getRejectionHandler: RejectionHandler = { |
| def complete(err: String): Route = completeError(StatusCodes.BadRequest, "NC_ERROR", err) |
| |
| RejectionHandler.newBuilder(). |
| handle { |
| case _: UnsupportedRequestContentTypeRejection => complete("Unsupported content type.") |
| case _: SchemeRejection => complete("Unsupported scheme.") |
| case _: UnsatisfiableRangeRejection => complete("Invalid range.") |
| case _: TransformationRejection => complete("Transformation error.") |
| case _: MalformedRequestContentRejection => complete("Malformed or missing content.") |
| case _: AuthenticationFailedRejection => complete("Authentication error.") |
| case _: TooManyRangesRejection => complete("Too many ranges.") |
| case err: MissingFormFieldRejection => complete(s"Missing form field: ${err.fieldName}") |
| case _: UnacceptedResponseContentTypeRejection => complete(s"Unsupported response content type.") |
| case _: MethodRejection => complete(s"Invalid method.") |
| case _: UnacceptedResponseEncodingRejection => complete(s"Unsupported response encoding.") |
| case _: UnsupportedRequestEncodingRejection => complete(s"Unsupported request encoding.") |
| case err: MissingCookieRejection => complete(s"Missing cookie: ${err.cookieName}'") |
| case err: InvalidRequiredValueForQueryParamRejection => complete(s"Invalid parameter value: ${err.parameterName}") |
| case _: UnsupportedWebSocketSubprotocolRejection => complete(s"Unsupported web socket sub-protocol.") |
| case _: CircuitBreakerOpenRejection => complete(s"Circuit breaker error.") |
| case _: MissingHeaderRejection => complete(s"Missing header.") |
| case err: MissingQueryParamRejection => complete(s"Missing parameter: ${err.parameterName}") |
| case _: InvalidOriginRejection => complete(s"Invalid origin.") |
| |
| // Common case. |
| case _: Rejection => complete(s"Bad request.") |
| }.result() |
| } |
| |
| /** |
| * |
| * @param m |
| * @param f |
| * @return |
| */ |
| private def withMetric(m: Measure, f: () => Route): Route = { |
| val start = U.now() |
| |
| try |
| f() |
| finally { |
| recordStats(m -> (U.now() - start)) |
| } |
| } |
| |
| /** |
| * @return |
| */ |
| private implicit def getContext: ExecutionContext = NCThreadPoolManager.getContext("probes.communication") |
| |
| /** |
| * |
| * @return |
| */ |
| override def getRoute: Route = { |
| val timeoutResp = HttpResponse( |
| StatusCodes.EnhanceYourCalm, |
| entity = "Unable to serve response within time limit." |
| ) |
| |
| handleExceptions(getExceptionHandler) { |
| handleRejections(getRejectionHandler) { |
| corsHandler ( |
| get { |
| withRequestTimeoutResponse(_ => timeoutResp) { |
| path(API / "health") { $health() } // Also duplicated for POST. |
| } |
| } ~ |
| post { |
| encodeResponseWith(Coders.NoCoding, Coders.Gzip) { |
| withRequestTimeoutResponse(_ => timeoutResp) { |
| path(API / "health") { $health() } // Duplicate for POST. |
| path(API / "signin") { withMetric(M_SIGNIN_LATENCY_MS, $signin) } ~ |
| path(API / "signout") { withMetric(M_SIGNOUT_LATENCY_MS, $signout) } ~ |
| path(API / "cancel") { withMetric(M_CANCEL_LATENCY_MS, $cancel) } ~ |
| path(API / "check") { withMetric(M_CHECK_LATENCY_MS, $check) } ~ |
| path(API / "clear"/ "conversation") { withMetric(M_CLEAR_CONV_LATENCY_MS, $clear$Conversation) } ~ |
| path(API / "clear"/ "dialog") { withMetric(M_CLEAR_DIALOG_LATENCY_MS, $clear$Dialog) } ~ |
| path(API / "company"/ "add") { withMetric(M_COMPANY_ADD_LATENCY_MS, $company$Add) } ~ |
| path(API / "company"/ "get") { withMetric(M_COMPANY_GET_LATENCY_MS, $company$Get) } ~ |
| path(API / "company" / "update") { withMetric(M_COMPANY_UPDATE_LATENCY_MS, $company$Update) } ~ |
| path(API / "company" / "token" / "reset") { withMetric(M_COMPANY_TOKEN_LATENCY_MS, $company$Token$Reset) } ~ |
| path(API / "company" / "delete") { withMetric(M_COMPANY_DELETE_LATENCY_MS, $company$Delete) } ~ |
| path(API / "user" / "get") { withMetric(M_USER_GET_LATENCY_MS, $user$Get) } ~ |
| path(API / "user" / "add") { withMetric(M_USER_ADD_LATENCY_MS, $user$Add) } ~ |
| path(API / "user" / "update") { withMetric(M_USER_UPDATE_LATENCY_MS, $user$Update) } ~ |
| path(API / "user" / "delete") { withMetric(M_USER_DELETE_LATENCY_MS, $user$Delete) } ~ |
| path(API / "user" / "admin") { withMetric(M_USER_ADMIN_LATENCY_MS, $user$Admin) } ~ |
| path(API / "user" / "passwd" / "reset") { withMetric(M_USER_PASSWD_RESET_LATENCY_MS, $user$Passwd$Reset) } ~ |
| path(API / "user" / "all") { withMetric(M_USER_ALL_LATENCY_MS, $user$All) } ~ |
| path(API / "feedback"/ "add") { withMetric(M_FEEDBACK_ADD_LATENCY_MS, $feedback$Add) } ~ |
| path(API / "feedback"/ "all") { withMetric(M_FEEDBACK_GET_LATENCY_MS, $feedback$All) } ~ |
| path(API / "feedback" / "delete") { withMetric(M_FEEDBACK_DELETE_LATENCY_MS, $feedback$Delete) } ~ |
| path(API / "probe" / "all") { withMetric(M_PROBE_ALL_LATENCY_MS, $probe$All) } ~ |
| path(API / "model" / "sugsyn") { withMetric(M_MODEL_SUGSYN_LATENCY_MS, $model$sugsyn) } ~ |
| path(API / "model" / "syns") { withMetric(M_MODEL_SYNS_LATENCY_MS, $model$syns) } ~ |
| path(API / "ask") { withMetric(M_ASK_LATENCY_MS, $ask) } ~ |
| path(API / "ask" / "sync") { withMetric(M_ASK_SYNC_LATENCY_MS, $ask$Sync) } |
| } |
| } |
| } |
| ) |
| } |
| } |
| } |
| } |