feat(account): enhance account management with user and bot account functionalities
This commit is contained in:
+11
-2
@@ -1,13 +1,15 @@
|
|||||||
package de.nowchess.account.config
|
package de.nowchess.account.config
|
||||||
|
|
||||||
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
||||||
import de.nowchess.account.domain.{Account, Challenge, ChallengeColor, ChallengeStatus, DeclineReason, TimeControl}
|
import de.nowchess.account.domain.{UserAccount, BotAccount, OfficialBotAccount, Challenge, ChallengeColor, ChallengeStatus, DeclineReason, TimeControl}
|
||||||
import de.nowchess.account.dto.*
|
import de.nowchess.account.dto.*
|
||||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||||
|
|
||||||
@RegisterForReflection(
|
@RegisterForReflection(
|
||||||
targets = Array(
|
targets = Array(
|
||||||
classOf[Account],
|
classOf[UserAccount],
|
||||||
|
classOf[BotAccount],
|
||||||
|
classOf[OfficialBotAccount],
|
||||||
classOf[Challenge],
|
classOf[Challenge],
|
||||||
classOf[ChallengeColor],
|
classOf[ChallengeColor],
|
||||||
classOf[ChallengeStatus],
|
classOf[ChallengeStatus],
|
||||||
@@ -17,6 +19,13 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
|||||||
classOf[TokenResponse],
|
classOf[TokenResponse],
|
||||||
classOf[PlayerInfo],
|
classOf[PlayerInfo],
|
||||||
classOf[PublicAccountDto],
|
classOf[PublicAccountDto],
|
||||||
|
classOf[BotAccountDto],
|
||||||
|
classOf[BotAccountWithTokenDto],
|
||||||
|
classOf[OfficialBotAccountDto],
|
||||||
|
classOf[OfficialBotAccountWithTokenDto],
|
||||||
|
classOf[CreateBotAccountRequest],
|
||||||
|
classOf[UpdateBotNameRequest],
|
||||||
|
classOf[RotatedTokenDto],
|
||||||
classOf[TimeControlDto],
|
classOf[TimeControlDto],
|
||||||
classOf[ChallengeRequest],
|
classOf[ChallengeRequest],
|
||||||
classOf[ChallengeDto],
|
classOf[ChallengeDto],
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package de.nowchess.account.domain
|
|
||||||
|
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase
|
|
||||||
import jakarta.persistence.*
|
|
||||||
import scala.compiletime.uninitialized
|
|
||||||
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "accounts")
|
|
||||||
class Account extends PanacheEntityBase:
|
|
||||||
// scalafix:off DisableSyntax.var
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.UUID)
|
|
||||||
var id: UUID = uninitialized
|
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
|
||||||
var username: String = uninitialized
|
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
|
||||||
var email: String = uninitialized
|
|
||||||
|
|
||||||
var passwordHash: String = uninitialized
|
|
||||||
|
|
||||||
var rating: Int = 1500
|
|
||||||
|
|
||||||
var createdAt: Instant = uninitialized
|
|
||||||
// scalafix:on
|
|
||||||
@@ -17,10 +17,10 @@ class Challenge extends PanacheEntityBase:
|
|||||||
var id: UUID = uninitialized
|
var id: UUID = uninitialized
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
var challenger: Account = uninitialized
|
var challenger: UserAccount = uninitialized
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
var destUser: Account = uninitialized
|
var destUser: UserAccount = uninitialized
|
||||||
|
|
||||||
@Convert(converter = classOf[ChallengeColorConverter])
|
@Convert(converter = classOf[ChallengeColorConverter])
|
||||||
@Column(columnDefinition = "varchar(255)")
|
@Column(columnDefinition = "varchar(255)")
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package de.nowchess.account.domain
|
||||||
|
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheEntityBase
|
||||||
|
import jakarta.persistence.*
|
||||||
|
import scala.compiletime.uninitialized
|
||||||
|
import scala.jdk.CollectionConverters.*
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "user_accounts")
|
||||||
|
class UserAccount extends PanacheEntityBase:
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
var id: UUID = uninitialized
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
|
var username: String = uninitialized
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
|
var email: String = uninitialized
|
||||||
|
|
||||||
|
var passwordHash: String = uninitialized
|
||||||
|
|
||||||
|
var rating: Int = 1500
|
||||||
|
|
||||||
|
var createdAt: Instant = uninitialized
|
||||||
|
|
||||||
|
var banned: Boolean = false
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "owner", cascade = Array(CascadeType.ALL), orphanRemoval = true)
|
||||||
|
var botAccounts: java.util.List[BotAccount] = uninitialized
|
||||||
|
// scalafix:on
|
||||||
|
|
||||||
|
def getBotAccounts: List[BotAccount] = Option(botAccounts).map(_.asScala.toList).getOrElse(Nil)
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "bot_accounts")
|
||||||
|
class BotAccount extends PanacheEntityBase:
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
var id: UUID = uninitialized
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var name: String = uninitialized
|
||||||
|
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
@JoinColumn(name = "owner_id", nullable = false)
|
||||||
|
var owner: UserAccount = uninitialized
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false, length = 256)
|
||||||
|
var token: String = uninitialized
|
||||||
|
|
||||||
|
var rating: Int = 1500
|
||||||
|
|
||||||
|
var createdAt: Instant = uninitialized
|
||||||
|
|
||||||
|
var banned: Boolean = false
|
||||||
|
// scalafix:on
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "official_bot_accounts")
|
||||||
|
class OfficialBotAccount extends PanacheEntityBase:
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
var id: UUID = uninitialized
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var name: String = uninitialized
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false, length = 256)
|
||||||
|
var token: String = uninitialized
|
||||||
|
|
||||||
|
var rating: Int = 1500
|
||||||
|
|
||||||
|
var createdAt: Instant = uninitialized
|
||||||
|
// scalafix:on
|
||||||
@@ -33,3 +33,17 @@ case class DeclineRequest(reason: Option[String])
|
|||||||
case class ChallengeListDto(in: List[ChallengeDto], out: List[ChallengeDto])
|
case class ChallengeListDto(in: List[ChallengeDto], out: List[ChallengeDto])
|
||||||
|
|
||||||
case class ErrorDto(error: String)
|
case class ErrorDto(error: String)
|
||||||
|
|
||||||
|
case class CreateBotAccountRequest(name: String)
|
||||||
|
|
||||||
|
case class UpdateBotNameRequest(name: String)
|
||||||
|
|
||||||
|
case class BotAccountDto(id: String, name: String, rating: Int, createdAt: String)
|
||||||
|
|
||||||
|
case class BotAccountWithTokenDto(id: String, name: String, rating: Int, token: String, createdAt: String)
|
||||||
|
|
||||||
|
case class RotatedTokenDto(token: String)
|
||||||
|
|
||||||
|
case class OfficialBotAccountDto(id: String, name: String, rating: Int, createdAt: String)
|
||||||
|
|
||||||
|
case class OfficialBotAccountWithTokenDto(id: String, name: String, rating: Int, token: String, createdAt: String)
|
||||||
|
|||||||
@@ -4,8 +4,20 @@ enum AccountError:
|
|||||||
case UsernameTaken(username: String)
|
case UsernameTaken(username: String)
|
||||||
case EmailAlreadyRegistered(email: String)
|
case EmailAlreadyRegistered(email: String)
|
||||||
case InvalidCredentials
|
case InvalidCredentials
|
||||||
|
case UserNotFound
|
||||||
|
case BotNotFound
|
||||||
|
case BotLimitExceeded
|
||||||
|
case NotAuthorized
|
||||||
|
case UserBanned
|
||||||
|
case BotBanned
|
||||||
|
|
||||||
def message: String = this match
|
def message: String = this match
|
||||||
case UsernameTaken(u) => s"Username '$u' is already taken"
|
case UsernameTaken(u) => s"Username '$u' is already taken"
|
||||||
case EmailAlreadyRegistered(e) => s"Email '$e' is already registered"
|
case EmailAlreadyRegistered(e) => s"Email '$e' is already registered"
|
||||||
case InvalidCredentials => "Invalid credentials"
|
case InvalidCredentials => "Invalid credentials"
|
||||||
|
case UserNotFound => "User not found"
|
||||||
|
case BotNotFound => "Bot account not found"
|
||||||
|
case BotLimitExceeded => "Maximum of 5 bot accounts per user exceeded"
|
||||||
|
case NotAuthorized => "Not authorized to perform this action"
|
||||||
|
case UserBanned => "User account is banned"
|
||||||
|
case BotBanned => "Bot account is banned"
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package de.nowchess.account.filter
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
|
import jakarta.inject.Inject
|
||||||
|
import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter}
|
||||||
|
import jakarta.ws.rs.core.Response
|
||||||
|
import jakarta.ws.rs.ext.Provider
|
||||||
|
import org.eclipse.microprofile.jwt.JsonWebToken
|
||||||
|
import scala.compiletime.uninitialized
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
@ApplicationScoped
|
||||||
|
class AlreadyLoggedInFilter extends ContainerRequestFilter:
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
var jwt: JsonWebToken = uninitialized
|
||||||
|
// scalafix:on
|
||||||
|
|
||||||
|
override def filter(context: ContainerRequestContext): Unit =
|
||||||
|
val path = context.getUriInfo.getPath
|
||||||
|
val method = context.getMethod
|
||||||
|
|
||||||
|
if isProtectedEndpoint(path, method) && isAuthenticated then
|
||||||
|
context.abortWith(
|
||||||
|
Response
|
||||||
|
.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity("""{"error":"Already logged in"}""")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
private def isAuthenticated: Boolean =
|
||||||
|
try jwt.getName != null
|
||||||
|
catch case _ => false
|
||||||
|
|
||||||
|
private def isProtectedEndpoint(path: String, method: String): Boolean =
|
||||||
|
(path.contains("/api/account") || path.contains("/account")) &&
|
||||||
|
((path.endsWith("/api/account") && method == "POST") ||
|
||||||
|
(path.endsWith("/account") && method == "POST") ||
|
||||||
|
(path.contains("/login") && method == "POST"))
|
||||||
+75
-11
@@ -1,6 +1,6 @@
|
|||||||
package de.nowchess.account.repository
|
package de.nowchess.account.repository
|
||||||
|
|
||||||
import de.nowchess.account.domain.Account
|
import de.nowchess.account.domain.{UserAccount, BotAccount, OfficialBotAccount}
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import jakarta.persistence.EntityManager
|
import jakarta.persistence.EntityManager
|
||||||
@@ -9,15 +9,15 @@ import java.util.UUID
|
|||||||
import scala.jdk.CollectionConverters.*
|
import scala.jdk.CollectionConverters.*
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
class AccountRepository:
|
class UserAccountRepository:
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
var em: EntityManager = scala.compiletime.uninitialized
|
var em: EntityManager = scala.compiletime.uninitialized
|
||||||
// scalafix:on
|
// scalafix:on
|
||||||
|
|
||||||
def findByUsername(username: String): Option[Account] =
|
def findByUsername(username: String): Option[UserAccount] =
|
||||||
em.createQuery("FROM Account WHERE username = :username", classOf[Account])
|
em.createQuery("FROM UserAccount WHERE username = :username", classOf[UserAccount])
|
||||||
.setParameter("username", username)
|
.setParameter("username", username)
|
||||||
.getResultList
|
.getResultList
|
||||||
.stream()
|
.stream()
|
||||||
@@ -25,19 +25,83 @@ class AccountRepository:
|
|||||||
.map(Option(_))
|
.map(Option(_))
|
||||||
.orElse(None)
|
.orElse(None)
|
||||||
|
|
||||||
def findById(id: UUID): Option[Account] =
|
def findById(id: UUID): Option[UserAccount] =
|
||||||
Option(em.find(classOf[Account], id))
|
Option(em.find(classOf[UserAccount], id))
|
||||||
|
|
||||||
def persist(account: Account): Account =
|
def persist(account: UserAccount): UserAccount =
|
||||||
em.persist(account)
|
em.persist(account)
|
||||||
account
|
account
|
||||||
|
|
||||||
def findByEmail(email: String): Option[Account] =
|
def findByEmail(email: String): Option[UserAccount] =
|
||||||
em.createQuery("FROM Account WHERE email = :email", classOf[Account])
|
em.createQuery("FROM UserAccount WHERE email = :email", classOf[UserAccount])
|
||||||
.setParameter("email", email)
|
.setParameter("email", email)
|
||||||
.getResultList
|
.getResultList
|
||||||
.asScala
|
.asScala
|
||||||
.headOption
|
.headOption
|
||||||
|
|
||||||
def findAll(): List[Account] =
|
def findAll(): List[UserAccount] =
|
||||||
em.createQuery("FROM Account", classOf[Account]).getResultList.asScala.toList
|
em.createQuery("FROM UserAccount", classOf[UserAccount]).getResultList.asScala.toList
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
class BotAccountRepository:
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
var em: EntityManager = scala.compiletime.uninitialized
|
||||||
|
// scalafix:on
|
||||||
|
|
||||||
|
def findById(id: UUID): Option[BotAccount] =
|
||||||
|
Option(em.find(classOf[BotAccount], id))
|
||||||
|
|
||||||
|
def findByOwner(ownerId: UUID): List[BotAccount] =
|
||||||
|
em.createQuery("FROM BotAccount WHERE owner.id = :ownerId", classOf[BotAccount])
|
||||||
|
.setParameter("ownerId", ownerId)
|
||||||
|
.getResultList
|
||||||
|
.asScala
|
||||||
|
.toList
|
||||||
|
|
||||||
|
def persist(bot: BotAccount): BotAccount =
|
||||||
|
em.persist(bot)
|
||||||
|
bot
|
||||||
|
|
||||||
|
def delete(botId: UUID): Unit =
|
||||||
|
em.find(classOf[BotAccount], botId) match
|
||||||
|
case bot: BotAccount => em.remove(bot)
|
||||||
|
case _ => ()
|
||||||
|
|
||||||
|
def findByToken(token: String): Option[BotAccount] =
|
||||||
|
em.createQuery("FROM BotAccount WHERE token = :token", classOf[BotAccount])
|
||||||
|
.setParameter("token", token)
|
||||||
|
.getResultList
|
||||||
|
.asScala
|
||||||
|
.headOption
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
class OfficialBotAccountRepository:
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
var em: EntityManager = scala.compiletime.uninitialized
|
||||||
|
// scalafix:on
|
||||||
|
|
||||||
|
def findById(id: UUID): Option[OfficialBotAccount] =
|
||||||
|
Option(em.find(classOf[OfficialBotAccount], id))
|
||||||
|
|
||||||
|
def findAll(): List[OfficialBotAccount] =
|
||||||
|
em.createQuery("FROM OfficialBotAccount", classOf[OfficialBotAccount]).getResultList.asScala.toList
|
||||||
|
|
||||||
|
def persist(bot: OfficialBotAccount): OfficialBotAccount =
|
||||||
|
em.persist(bot)
|
||||||
|
bot
|
||||||
|
|
||||||
|
def delete(botId: UUID): Unit =
|
||||||
|
em.find(classOf[OfficialBotAccount], botId) match
|
||||||
|
case bot: OfficialBotAccount => em.remove(bot)
|
||||||
|
case _ => ()
|
||||||
|
|
||||||
|
def findByToken(token: String): Option[OfficialBotAccount] =
|
||||||
|
em.createQuery("FROM OfficialBotAccount WHERE token = :token", classOf[OfficialBotAccount])
|
||||||
|
.setParameter("token", token)
|
||||||
|
.getResultList
|
||||||
|
.asScala
|
||||||
|
.headOption
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package de.nowchess.account.resource
|
package de.nowchess.account.resource
|
||||||
|
|
||||||
import de.nowchess.account.domain.Account
|
import de.nowchess.account.domain.{UserAccount, BotAccount, OfficialBotAccount}
|
||||||
import de.nowchess.account.dto.*
|
import de.nowchess.account.dto.*
|
||||||
import de.nowchess.account.error.AccountError
|
import de.nowchess.account.error.AccountError
|
||||||
import de.nowchess.account.service.AccountService
|
import de.nowchess.account.service.AccountService
|
||||||
@@ -42,6 +42,8 @@ class AccountResource:
|
|||||||
accountService.login(req) match
|
accountService.login(req) match
|
||||||
case Right(token) =>
|
case Right(token) =>
|
||||||
Response.ok(TokenResponse(token)).build()
|
Response.ok(TokenResponse(token)).build()
|
||||||
|
case Left(AccountError.UserBanned) =>
|
||||||
|
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.UserBanned.message)).build()
|
||||||
case Left(error) =>
|
case Left(error) =>
|
||||||
Response.status(Response.Status.UNAUTHORIZED).entity(ErrorDto(error.message)).build()
|
Response.status(Response.Status.UNAUTHORIZED).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
@@ -61,10 +63,158 @@ class AccountResource:
|
|||||||
case Some(account) => Response.ok(toPublicDto(account)).build()
|
case Some(account) => Response.ok(toPublicDto(account)).build()
|
||||||
case None => Response.status(Response.Status.NOT_FOUND).build()
|
case None => Response.status(Response.Status.NOT_FOUND).build()
|
||||||
|
|
||||||
private def toPublicDto(account: Account): PublicAccountDto =
|
@POST
|
||||||
|
@Path("/{userId}/ban")
|
||||||
|
@RolesAllowed(Array("Admin"))
|
||||||
|
def banUser(@PathParam("userId") userId: String): Response =
|
||||||
|
accountService.banUser(UUID.fromString(userId)) match
|
||||||
|
case Right(user) => Response.ok(toPublicDto(user)).build()
|
||||||
|
case Left(error) =>
|
||||||
|
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/{userId}/unban")
|
||||||
|
@RolesAllowed(Array("Admin"))
|
||||||
|
def unbanUser(@PathParam("userId") userId: String): Response =
|
||||||
|
accountService.unbanUser(UUID.fromString(userId)) match
|
||||||
|
case Right(user) => Response.ok(toPublicDto(user)).build()
|
||||||
|
case Left(error) =>
|
||||||
|
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/bots")
|
||||||
|
@RolesAllowed(Array("**"))
|
||||||
|
def createBotAccount(req: CreateBotAccountRequest): Response =
|
||||||
|
val ownerId = UUID.fromString(jwt.getSubject)
|
||||||
|
accountService.createBotAccount(ownerId, req.name) match
|
||||||
|
case Right(bot) =>
|
||||||
|
Response.status(Response.Status.CREATED).entity(toBotDtoWithToken(bot)).build()
|
||||||
|
case Left(error) =>
|
||||||
|
val status = error match
|
||||||
|
case AccountError.BotLimitExceeded => Response.Status.BAD_REQUEST
|
||||||
|
case _ => Response.Status.INTERNAL_SERVER_ERROR
|
||||||
|
Response.status(status).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/bots")
|
||||||
|
@RolesAllowed(Array("**"))
|
||||||
|
def listBotAccounts(): Response =
|
||||||
|
val ownerId = UUID.fromString(jwt.getSubject)
|
||||||
|
val bots = accountService.getBotAccounts(ownerId)
|
||||||
|
Response.ok(bots.map(toBotDto)).build()
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Path("/bots/{botId}")
|
||||||
|
@RolesAllowed(Array("**"))
|
||||||
|
def updateBotName(@PathParam("botId") botId: String, req: UpdateBotNameRequest): Response =
|
||||||
|
val ownerId = UUID.fromString(jwt.getSubject)
|
||||||
|
accountService.updateBotName(UUID.fromString(botId), ownerId, req.name) match
|
||||||
|
case Right(bot) => Response.ok(toBotDto(bot)).build()
|
||||||
|
case Left(AccountError.NotAuthorized) =>
|
||||||
|
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.NotAuthorized.message)).build()
|
||||||
|
case Left(error) =>
|
||||||
|
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/bots/{botId}/rotate-token")
|
||||||
|
@RolesAllowed(Array("**"))
|
||||||
|
def rotateBotToken(@PathParam("botId") botId: String): Response =
|
||||||
|
val ownerId = UUID.fromString(jwt.getSubject)
|
||||||
|
accountService.rotateBotToken(UUID.fromString(botId), ownerId) match
|
||||||
|
case Right(bot) => Response.ok(RotatedTokenDto(bot.token)).build()
|
||||||
|
case Left(AccountError.NotAuthorized) =>
|
||||||
|
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.NotAuthorized.message)).build()
|
||||||
|
case Left(error) =>
|
||||||
|
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/bots/{botId}")
|
||||||
|
@RolesAllowed(Array("**"))
|
||||||
|
def deleteBotAccount(@PathParam("botId") botId: String): Response =
|
||||||
|
val ownerId = UUID.fromString(jwt.getSubject)
|
||||||
|
val botUuid = UUID.fromString(botId)
|
||||||
|
accountService.getBotAccountWithOwnerCheck(botUuid, ownerId) match
|
||||||
|
case None => Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(AccountError.BotNotFound.message)).build()
|
||||||
|
case Some(None) =>
|
||||||
|
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.NotAuthorized.message)).build()
|
||||||
|
case Some(Some(_)) =>
|
||||||
|
accountService.deleteBotAccount(botUuid) match
|
||||||
|
case Right(_) => Response.noContent().build()
|
||||||
|
case Left(error) =>
|
||||||
|
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
private def toPublicDto(account: UserAccount): PublicAccountDto =
|
||||||
PublicAccountDto(
|
PublicAccountDto(
|
||||||
id = account.id.toString,
|
id = account.id.toString,
|
||||||
username = account.username,
|
username = account.username,
|
||||||
rating = account.rating,
|
rating = account.rating,
|
||||||
createdAt = account.createdAt.toString,
|
createdAt = account.createdAt.toString,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private def toBotDto(bot: BotAccount): BotAccountDto =
|
||||||
|
BotAccountDto(
|
||||||
|
id = bot.id.toString,
|
||||||
|
name = bot.name,
|
||||||
|
rating = bot.rating,
|
||||||
|
createdAt = bot.createdAt.toString,
|
||||||
|
)
|
||||||
|
|
||||||
|
private def toBotDtoWithToken(bot: BotAccount): BotAccountWithTokenDto =
|
||||||
|
BotAccountWithTokenDto(
|
||||||
|
id = bot.id.toString,
|
||||||
|
name = bot.name,
|
||||||
|
rating = bot.rating,
|
||||||
|
token = bot.token,
|
||||||
|
createdAt = bot.createdAt.toString,
|
||||||
|
)
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/official-bots")
|
||||||
|
def getOfficialBots(): Response =
|
||||||
|
val bots = accountService.getOfficialBotAccounts()
|
||||||
|
Response.ok(bots.map(toOfficialBotDto)).build()
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/official-bots")
|
||||||
|
@RolesAllowed(Array("Admin"))
|
||||||
|
def createOfficialBot(req: CreateBotAccountRequest): Response =
|
||||||
|
accountService.createOfficialBotAccount(req.name) match
|
||||||
|
case Right(bot) =>
|
||||||
|
Response.status(Response.Status.CREATED).entity(toOfficialBotDtoWithToken(bot)).build()
|
||||||
|
case Left(error) =>
|
||||||
|
Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
@DELETE
|
||||||
|
@Path("/official-bots/{botId}")
|
||||||
|
@RolesAllowed(Array("Admin"))
|
||||||
|
def deleteOfficialBot(@PathParam("botId") botId: String): Response =
|
||||||
|
accountService.deleteOfficialBotAccount(UUID.fromString(botId)) match
|
||||||
|
case Right(_) => Response.noContent().build()
|
||||||
|
case Left(error) =>
|
||||||
|
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/official-bots/{botId}/rotate-token")
|
||||||
|
@RolesAllowed(Array("Admin"))
|
||||||
|
def rotateOfficialBotToken(@PathParam("botId") botId: String): Response =
|
||||||
|
accountService.rotateOfficialBotToken(UUID.fromString(botId)) match
|
||||||
|
case Right(bot) => Response.ok(RotatedTokenDto(bot.token)).build()
|
||||||
|
case Left(error) =>
|
||||||
|
Response.status(Response.Status.NOT_FOUND).entity(ErrorDto(error.message)).build()
|
||||||
|
|
||||||
|
private def toOfficialBotDto(bot: OfficialBotAccount): OfficialBotAccountDto =
|
||||||
|
OfficialBotAccountDto(
|
||||||
|
id = bot.id.toString,
|
||||||
|
name = bot.name,
|
||||||
|
rating = bot.rating,
|
||||||
|
createdAt = bot.createdAt.toString,
|
||||||
|
)
|
||||||
|
|
||||||
|
private def toOfficialBotDtoWithToken(bot: OfficialBotAccount): OfficialBotAccountWithTokenDto =
|
||||||
|
OfficialBotAccountWithTokenDto(
|
||||||
|
id = bot.id.toString,
|
||||||
|
name = bot.name,
|
||||||
|
rating = bot.rating,
|
||||||
|
token = bot.token,
|
||||||
|
createdAt = bot.createdAt.toString,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package de.nowchess.account.service
|
package de.nowchess.account.service
|
||||||
|
|
||||||
import de.nowchess.account.domain.Account
|
import de.nowchess.account.domain.{UserAccount, BotAccount, OfficialBotAccount}
|
||||||
import de.nowchess.account.dto.{LoginRequest, RegisterRequest}
|
import de.nowchess.account.dto.{LoginRequest, RegisterRequest}
|
||||||
import de.nowchess.account.error.AccountError
|
import de.nowchess.account.error.AccountError
|
||||||
import de.nowchess.account.repository.AccountRepository
|
import de.nowchess.account.repository.{UserAccountRepository, BotAccountRepository, OfficialBotAccountRepository}
|
||||||
import io.quarkus.elytron.security.common.BcryptUtil
|
import io.quarkus.elytron.security.common.BcryptUtil
|
||||||
import io.smallrye.jwt.build.Jwt
|
import io.smallrye.jwt.build.Jwt
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
@@ -19,37 +19,164 @@ class AccountService:
|
|||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@Inject
|
@Inject
|
||||||
var accountRepository: AccountRepository = uninitialized
|
var userAccountRepository: UserAccountRepository = uninitialized
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
var botAccountRepository: BotAccountRepository = uninitialized
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
var officialBotAccountRepository: OfficialBotAccountRepository = uninitialized
|
||||||
// scalafix:on
|
// scalafix:on
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
def register(req: RegisterRequest): Either[AccountError, Account] =
|
def register(req: RegisterRequest): Either[AccountError, UserAccount] =
|
||||||
if accountRepository.findByUsername(req.username).isDefined then Left(AccountError.UsernameTaken(req.username))
|
if userAccountRepository.findByUsername(req.username).isDefined then Left(AccountError.UsernameTaken(req.username))
|
||||||
else if accountRepository.findByEmail(req.email).isDefined then Left(AccountError.EmailAlreadyRegistered(req.email))
|
else if userAccountRepository.findByEmail(req.email).isDefined then Left(AccountError.EmailAlreadyRegistered(req.email))
|
||||||
else
|
else
|
||||||
val account = new Account()
|
val account = new UserAccount()
|
||||||
account.username = req.username
|
account.username = req.username
|
||||||
account.email = req.email
|
account.email = req.email
|
||||||
account.passwordHash = BcryptUtil.bcryptHash(req.password)
|
account.passwordHash = BcryptUtil.bcryptHash(req.password)
|
||||||
account.createdAt = Instant.now()
|
account.createdAt = Instant.now()
|
||||||
accountRepository.persist(account)
|
userAccountRepository.persist(account)
|
||||||
Right(account)
|
Right(account)
|
||||||
|
|
||||||
def login(req: LoginRequest): Either[AccountError, String] =
|
def login(req: LoginRequest): Either[AccountError, String] =
|
||||||
accountRepository
|
userAccountRepository.findByUsername(req.username) match
|
||||||
.findByUsername(req.username)
|
case None => Left(AccountError.InvalidCredentials)
|
||||||
.filter(a => BcryptUtil.matches(req.password, a.passwordHash))
|
case Some(account) =>
|
||||||
.map { account =>
|
if !BcryptUtil.matches(req.password, account.passwordHash) then Left(AccountError.InvalidCredentials)
|
||||||
|
else if account.banned then Left(AccountError.UserBanned)
|
||||||
|
else
|
||||||
|
Right(
|
||||||
Jwt
|
Jwt
|
||||||
.issuer("nowchess")
|
.issuer("nowchess")
|
||||||
.subject(account.id.toString)
|
.subject(account.id.toString)
|
||||||
.claim("username", account.username)
|
.claim("username", account.username)
|
||||||
.sign()
|
.sign()
|
||||||
}
|
)
|
||||||
.toRight(AccountError.InvalidCredentials)
|
|
||||||
|
|
||||||
def findByUsername(username: String): Option[Account] =
|
def findByUsername(username: String): Option[UserAccount] =
|
||||||
accountRepository.findByUsername(username)
|
userAccountRepository.findByUsername(username)
|
||||||
|
|
||||||
def findById(id: UUID): Option[Account] =
|
def findById(id: UUID): Option[UserAccount] =
|
||||||
accountRepository.findById(id)
|
userAccountRepository.findById(id)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def createBotAccount(ownerId: UUID, botName: String): Either[AccountError, BotAccount] =
|
||||||
|
userAccountRepository.findById(ownerId) match
|
||||||
|
case None => Left(AccountError.UserNotFound)
|
||||||
|
case Some(owner) =>
|
||||||
|
val botAccounts = botAccountRepository.findByOwner(ownerId)
|
||||||
|
if botAccounts.length >= 5 then Left(AccountError.BotLimitExceeded)
|
||||||
|
else
|
||||||
|
val bot = new BotAccount()
|
||||||
|
bot.name = botName
|
||||||
|
bot.owner = owner
|
||||||
|
bot.token = generateBotToken(bot.id)
|
||||||
|
bot.createdAt = Instant.now()
|
||||||
|
botAccountRepository.persist(bot)
|
||||||
|
Right(bot)
|
||||||
|
|
||||||
|
def getBotAccounts(ownerId: UUID): List[BotAccount] =
|
||||||
|
botAccountRepository.findByOwner(ownerId)
|
||||||
|
|
||||||
|
def getBotAccountWithOwnerCheck(botId: UUID, ownerId: UUID): Option[Option[BotAccount]] =
|
||||||
|
botAccountRepository.findById(botId) match
|
||||||
|
case None => Some(None)
|
||||||
|
case Some(bot) => Some(Option(bot).filter(_.owner.id == ownerId))
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def deleteBotAccount(botId: UUID): Either[AccountError, Unit] =
|
||||||
|
botAccountRepository.findById(botId) match
|
||||||
|
case None => Left(AccountError.BotNotFound)
|
||||||
|
case Some(_) =>
|
||||||
|
botAccountRepository.delete(botId)
|
||||||
|
Right(())
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def updateBotName(botId: UUID, ownerId: UUID, newName: String): Either[AccountError, BotAccount] =
|
||||||
|
botAccountRepository.findById(botId) match
|
||||||
|
case None => Left(AccountError.BotNotFound)
|
||||||
|
case Some(bot) =>
|
||||||
|
if bot.owner.id != ownerId then Left(AccountError.NotAuthorized)
|
||||||
|
else
|
||||||
|
bot.name = newName
|
||||||
|
botAccountRepository.persist(bot)
|
||||||
|
Right(bot)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def rotateBotToken(botId: UUID, ownerId: UUID): Either[AccountError, BotAccount] =
|
||||||
|
botAccountRepository.findById(botId) match
|
||||||
|
case None => Left(AccountError.BotNotFound)
|
||||||
|
case Some(bot) =>
|
||||||
|
if bot.owner.id != ownerId then Left(AccountError.NotAuthorized)
|
||||||
|
else
|
||||||
|
bot.token = generateBotToken(botId)
|
||||||
|
botAccountRepository.persist(bot)
|
||||||
|
Right(bot)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def createOfficialBotAccount(botName: String): Either[AccountError, OfficialBotAccount] =
|
||||||
|
val bot = new OfficialBotAccount()
|
||||||
|
bot.name = botName
|
||||||
|
bot.token = generateOfficialBotToken(bot.id)
|
||||||
|
bot.createdAt = Instant.now()
|
||||||
|
officialBotAccountRepository.persist(bot)
|
||||||
|
Right(bot)
|
||||||
|
|
||||||
|
def getOfficialBotAccounts(): List[OfficialBotAccount] =
|
||||||
|
officialBotAccountRepository.findAll()
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def deleteOfficialBotAccount(botId: UUID): Either[AccountError, Unit] =
|
||||||
|
officialBotAccountRepository.findById(botId) match
|
||||||
|
case None => Left(AccountError.BotNotFound)
|
||||||
|
case Some(_) =>
|
||||||
|
officialBotAccountRepository.delete(botId)
|
||||||
|
Right(())
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def rotateOfficialBotToken(botId: UUID): Either[AccountError, OfficialBotAccount] =
|
||||||
|
officialBotAccountRepository.findById(botId) match
|
||||||
|
case None => Left(AccountError.BotNotFound)
|
||||||
|
case Some(bot) =>
|
||||||
|
bot.token = generateOfficialBotToken(botId)
|
||||||
|
officialBotAccountRepository.persist(bot)
|
||||||
|
Right(bot)
|
||||||
|
|
||||||
|
private def generateBotToken(botId: UUID): String =
|
||||||
|
Jwt
|
||||||
|
.issuer("nowchess")
|
||||||
|
.subject(botId.toString)
|
||||||
|
.expiresAt(Long.MaxValue)
|
||||||
|
.claim("type", "bot")
|
||||||
|
.sign()
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def banUser(userId: UUID): Either[AccountError, UserAccount] =
|
||||||
|
userAccountRepository.findById(userId) match
|
||||||
|
case None => Left(AccountError.UserNotFound)
|
||||||
|
case Some(user) =>
|
||||||
|
user.banned = true
|
||||||
|
user.botAccounts.forEach(_.banned = true)
|
||||||
|
userAccountRepository.persist(user)
|
||||||
|
Right(user)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
def unbanUser(userId: UUID): Either[AccountError, UserAccount] =
|
||||||
|
userAccountRepository.findById(userId) match
|
||||||
|
case None => Left(AccountError.UserNotFound)
|
||||||
|
case Some(user) =>
|
||||||
|
user.banned = false
|
||||||
|
user.botAccounts.forEach(_.banned = false)
|
||||||
|
userAccountRepository.persist(user)
|
||||||
|
Right(user)
|
||||||
|
|
||||||
|
private def generateOfficialBotToken(botId: UUID): String =
|
||||||
|
Jwt
|
||||||
|
.issuer("nowchess")
|
||||||
|
.subject(botId.toString)
|
||||||
|
.expiresAt(Long.MaxValue)
|
||||||
|
.claim("type", "official-bot")
|
||||||
|
.sign()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import de.nowchess.account.dto.{
|
|||||||
TimeControlDto,
|
TimeControlDto,
|
||||||
}
|
}
|
||||||
import de.nowchess.account.error.ChallengeError
|
import de.nowchess.account.error.ChallengeError
|
||||||
import de.nowchess.account.repository.{AccountRepository, ChallengeRepository}
|
import de.nowchess.account.repository.{UserAccountRepository, ChallengeRepository}
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import jakarta.transaction.Transactional
|
import jakarta.transaction.Transactional
|
||||||
@@ -33,7 +33,7 @@ class ChallengeService:
|
|||||||
|
|
||||||
// scalafix:off DisableSyntax.var
|
// scalafix:off DisableSyntax.var
|
||||||
@Inject
|
@Inject
|
||||||
var accountRepository: AccountRepository = uninitialized
|
var userAccountRepository: UserAccountRepository = uninitialized
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
var challengeRepository: ChallengeRepository = uninitialized
|
var challengeRepository: ChallengeRepository = uninitialized
|
||||||
@@ -46,8 +46,8 @@ class ChallengeService:
|
|||||||
@Transactional
|
@Transactional
|
||||||
def create(challengerId: UUID, destUsername: String, req: ChallengeRequest): Either[ChallengeError, Challenge] =
|
def create(challengerId: UUID, destUsername: String, req: ChallengeRequest): Either[ChallengeError, Challenge] =
|
||||||
for
|
for
|
||||||
destUser <- accountRepository.findByUsername(destUsername).toRight(ChallengeError.UserNotFound(destUsername))
|
destUser <- userAccountRepository.findByUsername(destUsername).toRight(ChallengeError.UserNotFound(destUsername))
|
||||||
challenger <- accountRepository.findById(challengerId).toRight(ChallengeError.ChallengerNotFound)
|
challenger <- userAccountRepository.findById(challengerId).toRight(ChallengeError.ChallengerNotFound)
|
||||||
_ <- Either.cond(challenger.id != destUser.id, (), ChallengeError.CannotChallengeSelf)
|
_ <- Either.cond(challenger.id != destUser.id, (), ChallengeError.CannotChallengeSelf)
|
||||||
_ <- Either.cond(
|
_ <- Either.cond(
|
||||||
challengeRepository.findDuplicateChallenge(challengerId, destUser.id).isEmpty,
|
challengeRepository.findDuplicateChallenge(challengerId, destUser.id).isEmpty,
|
||||||
|
|||||||
Reference in New Issue
Block a user