feat(account): enhance account management with user and bot account functionalities

This commit is contained in:
2026-04-25 12:39:30 +02:00
parent b6be0cd249
commit ec09a1bdb9
11 changed files with 541 additions and 73 deletions
@@ -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"))
@@ -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)
Jwt else if account.banned then Left(AccountError.UserBanned)
.issuer("nowchess") else
.subject(account.id.toString) Right(
.claim("username", account.username) Jwt
.sign() .issuer("nowchess")
} .subject(account.id.toString)
.toRight(AccountError.InvalidCredentials) .claim("username", account.username)
.sign()
)
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,