MiniDigger/Hangar

View on GitHub
ore/app/controllers/Organizations.scala

Summary

Maintainability
A
2 hrs
Test Coverage
package controllers

import javax.inject.{Inject, Singleton}

import play.api.i18n.{Lang, MessagesApi}
import play.api.mvc.{Action, AnyContent}

import form.OreForms
import form.organization.{OrganizationMembersUpdate, OrganizationRoleSetBuilder}
import ore.auth.SpongeAuthApi
import ore.db.DbRef
import ore.db.access.ModelView
import ore.db.impl.OrePostgresDriver.api._
import ore.member.MembershipDossier
import ore.models.organization.Organization
import ore.models.user.role.OrganizationUserRole
import ore.permission.Permission
import util.syntax._
import views.{html => views}

import cats.data.OptionT
import cats.effect.IO
import cats.syntax.all._

/**
  * Controller for handling Organization based actions.
  */
@Singleton
class Organizations @Inject()(forms: OreForms)(
    implicit oreComponents: OreControllerComponents[IO],
    auth: SpongeAuthApi[IO],
    messagesApi: MessagesApi
) extends OreBaseController {

  private val createLimit: Int = this.config.ore.orgs.createLimit

  /**
    * Shows the creation panel for Organizations.
    *
    * @return Organization creation panel
    */
  def showCreator(): Action[AnyContent] = UserLock().asyncF { implicit request =>
    service
      .runDBIO((request.user.ownedOrganizations(ModelView.later(Organization)).size > this.createLimit).result)
      .map { limitReached =>
        if (limitReached)
          Redirect(ShowHome).withError(request.messages.apply("error.org.createLimit", this.createLimit))
        else {
          Ok(views.createOrganization())
        }
      }
  }

  /**
    * Creates a new organization from the submitted data.
    *
    * @return Redirect to organization page
    */
  def create(): Action[OrganizationRoleSetBuilder] =
    UserLock().asyncF(
      parse.form(forms.OrganizationCreate, onErrors = FormErrorLocalized(routes.Organizations.showCreator()))
    ) { implicit request =>
      val user     = request.user
      val failCall = routes.Organizations.showCreator()

      if (user.isLocked) {
        IO.pure(BadRequest)
      } else if (!this.config.ore.orgs.enabled) {
        IO.pure(Redirect(failCall).withError("error.org.disabled"))
      } else {
        service
          .runDBIO((user.ownedOrganizations(ModelView.later(Organization)).size >= this.createLimit).result)
          .flatMap { limitReached =>
            if (limitReached)
              IO.pure(BadRequest)
            else {
              val formData = request.body
              organizations
                .create(formData.name, user.id, formData.build())
                .fold(
                  error => Redirect(failCall).withErrors(error),
                  organization => Redirect(routes.Users.showProjects(organization.name, None))
                )
            }
          }
      }
    }

  /**
    * Sets the status of a pending Organization invite for the current user.
    *
    * @param id     Invite ID
    * @param status Invite status
    * @return       NotFound if invite doesn't exist, Ok otherwise
    */
  def setInviteStatus(id: DbRef[OrganizationUserRole], status: String): Action[AnyContent] =
    Authenticated.asyncF { implicit request =>
      request.user
        .organizationRoles(ModelView.now(OrganizationUserRole))
        .get(id)
        .semiflatMap { role =>
          import MembershipDossier._
          status match {
            case STATUS_DECLINE  => role.organization.flatMap(org => org.memberships.removeRole(org)(role.id)).as(Ok)
            case STATUS_ACCEPT   => service.update(role)(_.copy(isAccepted = true)).as(Ok)
            case STATUS_UNACCEPT => service.update(role)(_.copy(isAccepted = false)).as(Ok)
            case _               => IO.pure(BadRequest)
          }
        }
        .getOrElse(notFound)
    }

  /**
    * Updates an [[Organization]]'s avatar.
    *
    * @param organization Organization to update avatar of
    * @return             Redirect to auth or bad request
    */
  def updateAvatar(organization: String): Action[AnyContent] =
    AuthedOrganizationAction(organization, requireUnlock = true)
      .andThen(OrganizationPermissionAction(Permission.EditOrganizationSettings))
      .asyncF { implicit request =>
        implicit val lang: Lang = request.lang

        auth.getChangeAvatarToken(request.user.name, organization).value.map {
          case Left(_) =>
            Redirect(routes.Users.showProjects(organization, None)).withError(messagesApi("organization.avatarFailed"))
          case Right(token) =>
            Redirect(auth.changeAvatarUri(organization, token).toString())
        }
      }

  /**
    * Removes a member from an [[Organization]].
    *
    * @param organization Organization to update
    * @return             Redirect to Organization page
    */
  def removeMember(organization: String): Action[String] =
    AuthedOrganizationAction(organization, requireUnlock = true)
      .andThen(OrganizationPermissionAction(Permission.ManageOrganizationMembers))
      .asyncF(parse.form(forms.OrganizationMemberRemove)) { implicit request =>
        val res = for {
          user <- users.withName(request.body)
          _    <- OptionT.liftF(request.data.orga.memberships.removeMember(request.data.orga)(user.id))
        } yield Redirect(ShowUser(organization))

        res.getOrElse(BadRequest)
      }

  /**
    * Updates an [[Organization]]'s members.
    *
    * @param organization Organization to update
    * @return             Redirect to Organization page
    */
  def updateMembers(organization: String): Action[OrganizationMembersUpdate] =
    AuthedOrganizationAction(organization, requireUnlock = true)
      .andThen(OrganizationPermissionAction(Permission.ManageOrganizationMembers))(
        parse.form(forms.OrganizationUpdateMembers)
      )
      .asyncF { implicit request =>
        request.body.saveTo(request.data.orga).as(Redirect(ShowUser(organization)))
      }
}