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
|
||||
|
||||
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 io.quarkus.runtime.annotations.RegisterForReflection
|
||||
|
||||
@RegisterForReflection(
|
||||
targets = Array(
|
||||
classOf[Account],
|
||||
classOf[UserAccount],
|
||||
classOf[BotAccount],
|
||||
classOf[OfficialBotAccount],
|
||||
classOf[Challenge],
|
||||
classOf[ChallengeColor],
|
||||
classOf[ChallengeStatus],
|
||||
@@ -17,6 +19,13 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
classOf[TokenResponse],
|
||||
classOf[PlayerInfo],
|
||||
classOf[PublicAccountDto],
|
||||
classOf[BotAccountDto],
|
||||
classOf[BotAccountWithTokenDto],
|
||||
classOf[OfficialBotAccountDto],
|
||||
classOf[OfficialBotAccountWithTokenDto],
|
||||
classOf[CreateBotAccountRequest],
|
||||
classOf[UpdateBotNameRequest],
|
||||
classOf[RotatedTokenDto],
|
||||
classOf[TimeControlDto],
|
||||
classOf[ChallengeRequest],
|
||||
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
|
||||
|
||||
@ManyToOne
|
||||
var challenger: Account = uninitialized
|
||||
var challenger: UserAccount = uninitialized
|
||||
|
||||
@ManyToOne
|
||||
var destUser: Account = uninitialized
|
||||
var destUser: UserAccount = uninitialized
|
||||
|
||||
@Convert(converter = classOf[ChallengeColorConverter])
|
||||
@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 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 EmailAlreadyRegistered(email: String)
|
||||
case InvalidCredentials
|
||||
case UserNotFound
|
||||
case BotNotFound
|
||||
case BotLimitExceeded
|
||||
case NotAuthorized
|
||||
case UserBanned
|
||||
case BotBanned
|
||||
|
||||
def message: String = this match
|
||||
case UsernameTaken(u) => s"Username '$u' is already taken"
|
||||
case EmailAlreadyRegistered(e) => s"Email '$e' is already registered"
|
||||
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
|
||||
|
||||
import de.nowchess.account.domain.Account
|
||||
import de.nowchess.account.domain.{UserAccount, BotAccount, OfficialBotAccount}
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.persistence.EntityManager
|
||||
@@ -9,15 +9,15 @@ import java.util.UUID
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
@ApplicationScoped
|
||||
class AccountRepository:
|
||||
class UserAccountRepository:
|
||||
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var em: EntityManager = scala.compiletime.uninitialized
|
||||
// scalafix:on
|
||||
|
||||
def findByUsername(username: String): Option[Account] =
|
||||
em.createQuery("FROM Account WHERE username = :username", classOf[Account])
|
||||
def findByUsername(username: String): Option[UserAccount] =
|
||||
em.createQuery("FROM UserAccount WHERE username = :username", classOf[UserAccount])
|
||||
.setParameter("username", username)
|
||||
.getResultList
|
||||
.stream()
|
||||
@@ -25,19 +25,83 @@ class AccountRepository:
|
||||
.map(Option(_))
|
||||
.orElse(None)
|
||||
|
||||
def findById(id: UUID): Option[Account] =
|
||||
Option(em.find(classOf[Account], id))
|
||||
def findById(id: UUID): Option[UserAccount] =
|
||||
Option(em.find(classOf[UserAccount], id))
|
||||
|
||||
def persist(account: Account): Account =
|
||||
def persist(account: UserAccount): UserAccount =
|
||||
em.persist(account)
|
||||
account
|
||||
|
||||
def findByEmail(email: String): Option[Account] =
|
||||
em.createQuery("FROM Account WHERE email = :email", classOf[Account])
|
||||
def findByEmail(email: String): Option[UserAccount] =
|
||||
em.createQuery("FROM UserAccount WHERE email = :email", classOf[UserAccount])
|
||||
.setParameter("email", email)
|
||||
.getResultList
|
||||
.asScala
|
||||
.headOption
|
||||
|
||||
def findAll(): List[Account] =
|
||||
em.createQuery("FROM Account", classOf[Account]).getResultList.asScala.toList
|
||||
def findAll(): List[UserAccount] =
|
||||
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
|
||||
|
||||
import de.nowchess.account.domain.Account
|
||||
import de.nowchess.account.domain.{UserAccount, BotAccount, OfficialBotAccount}
|
||||
import de.nowchess.account.dto.*
|
||||
import de.nowchess.account.error.AccountError
|
||||
import de.nowchess.account.service.AccountService
|
||||
@@ -42,6 +42,8 @@ class AccountResource:
|
||||
accountService.login(req) match
|
||||
case Right(token) =>
|
||||
Response.ok(TokenResponse(token)).build()
|
||||
case Left(AccountError.UserBanned) =>
|
||||
Response.status(Response.Status.FORBIDDEN).entity(ErrorDto(AccountError.UserBanned.message)).build()
|
||||
case Left(error) =>
|
||||
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 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(
|
||||
id = account.id.toString,
|
||||
username = account.username,
|
||||
rating = account.rating,
|
||||
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
|
||||
|
||||
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.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.smallrye.jwt.build.Jwt
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
@@ -19,37 +19,164 @@ class AccountService:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject
|
||||
var accountRepository: AccountRepository = uninitialized
|
||||
var userAccountRepository: UserAccountRepository = uninitialized
|
||||
|
||||
@Inject
|
||||
var botAccountRepository: BotAccountRepository = uninitialized
|
||||
|
||||
@Inject
|
||||
var officialBotAccountRepository: OfficialBotAccountRepository = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
@Transactional
|
||||
def register(req: RegisterRequest): Either[AccountError, Account] =
|
||||
if accountRepository.findByUsername(req.username).isDefined then Left(AccountError.UsernameTaken(req.username))
|
||||
else if accountRepository.findByEmail(req.email).isDefined then Left(AccountError.EmailAlreadyRegistered(req.email))
|
||||
def register(req: RegisterRequest): Either[AccountError, UserAccount] =
|
||||
if userAccountRepository.findByUsername(req.username).isDefined then Left(AccountError.UsernameTaken(req.username))
|
||||
else if userAccountRepository.findByEmail(req.email).isDefined then Left(AccountError.EmailAlreadyRegistered(req.email))
|
||||
else
|
||||
val account = new Account()
|
||||
val account = new UserAccount()
|
||||
account.username = req.username
|
||||
account.email = req.email
|
||||
account.passwordHash = BcryptUtil.bcryptHash(req.password)
|
||||
account.createdAt = Instant.now()
|
||||
accountRepository.persist(account)
|
||||
userAccountRepository.persist(account)
|
||||
Right(account)
|
||||
|
||||
def login(req: LoginRequest): Either[AccountError, String] =
|
||||
accountRepository
|
||||
.findByUsername(req.username)
|
||||
.filter(a => BcryptUtil.matches(req.password, a.passwordHash))
|
||||
.map { account =>
|
||||
Jwt
|
||||
.issuer("nowchess")
|
||||
.subject(account.id.toString)
|
||||
.claim("username", account.username)
|
||||
.sign()
|
||||
}
|
||||
.toRight(AccountError.InvalidCredentials)
|
||||
userAccountRepository.findByUsername(req.username) match
|
||||
case None => Left(AccountError.InvalidCredentials)
|
||||
case Some(account) =>
|
||||
if !BcryptUtil.matches(req.password, account.passwordHash) then Left(AccountError.InvalidCredentials)
|
||||
else if account.banned then Left(AccountError.UserBanned)
|
||||
else
|
||||
Right(
|
||||
Jwt
|
||||
.issuer("nowchess")
|
||||
.subject(account.id.toString)
|
||||
.claim("username", account.username)
|
||||
.sign()
|
||||
)
|
||||
|
||||
def findByUsername(username: String): Option[Account] =
|
||||
accountRepository.findByUsername(username)
|
||||
def findByUsername(username: String): Option[UserAccount] =
|
||||
userAccountRepository.findByUsername(username)
|
||||
|
||||
def findById(id: UUID): Option[Account] =
|
||||
accountRepository.findById(id)
|
||||
def findById(id: UUID): Option[UserAccount] =
|
||||
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,
|
||||
}
|
||||
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.inject.Inject
|
||||
import jakarta.transaction.Transactional
|
||||
@@ -33,7 +33,7 @@ class ChallengeService:
|
||||
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Inject
|
||||
var accountRepository: AccountRepository = uninitialized
|
||||
var userAccountRepository: UserAccountRepository = uninitialized
|
||||
|
||||
@Inject
|
||||
var challengeRepository: ChallengeRepository = uninitialized
|
||||
@@ -46,8 +46,8 @@ class ChallengeService:
|
||||
@Transactional
|
||||
def create(challengerId: UUID, destUsername: String, req: ChallengeRequest): Either[ChallengeError, Challenge] =
|
||||
for
|
||||
destUser <- accountRepository.findByUsername(destUsername).toRight(ChallengeError.UserNotFound(destUsername))
|
||||
challenger <- accountRepository.findById(challengerId).toRight(ChallengeError.ChallengerNotFound)
|
||||
destUser <- userAccountRepository.findByUsername(destUsername).toRight(ChallengeError.UserNotFound(destUsername))
|
||||
challenger <- userAccountRepository.findById(challengerId).toRight(ChallengeError.ChallengerNotFound)
|
||||
_ <- Either.cond(challenger.id != destUser.id, (), ChallengeError.CannotChallengeSelf)
|
||||
_ <- Either.cond(
|
||||
challengeRepository.findDuplicateChallenge(challengerId, destUser.id).isEmpty,
|
||||
|
||||
Reference in New Issue
Block a user