diff --git a/modules/model/src/main/scala/gs/smolban/model/Status.scala b/modules/model/src/main/scala/gs/smolban/model/Status.scala index f62918d..b880796 100644 --- a/modules/model/src/main/scala/gs/smolban/model/Status.scala +++ b/modules/model/src/main/scala/gs/smolban/model/Status.scala @@ -1,38 +1,44 @@ package gs.smolban.model +import java.time.Instant + /** Enumeration that describes the status of a [[Ticket]] in Smolban. Smolban * does not yet support custom status/workflow. - * - * @param value - * The string value of the status. */ -sealed abstract class Status(val value: String) +sealed trait Status: + def name: String + def enteredAt: Instant object Status: /** This ticket is new, and ready to be started. New tickets may be put into * progress or canceled. */ - case object Ready extends Status("ready") + case class Ready(enteredAt: Instant) extends Status: + val name: String = "ready" /** This ticket is being worked on actively. In progress tickets can be * paused, completed, or canceled. */ - case object InProgress extends Status("in_progress") + case class InProgress(enteredAt: Instant) extends Status: + val name: String = "in_progress" /** This ticket was being worked on, but was temporarily stopped. Paused * tickets may be put into progress or canceled. */ - case object Paused extends Status("paused") + case class Paused(enteredAt: Instant) extends Status: + val name: String = "paused" /** This ticket was driven to completion. The work is done. Once in this * state, the status may no longer change. */ - case object Complete extends Status("complete") + case class Complete(enteredAt: Instant) extends Status: + val name: String = "complete" /** This ticket was canceled for some reason. Once in this state, the status * may no longer change. */ - case object Canceled extends Status("canceled") + case class Canceled(enteredAt: Instant) extends Status: + val name: String = "canceled" end Status diff --git a/modules/model/src/main/scala/gs/smolban/model/Ticket.scala b/modules/model/src/main/scala/gs/smolban/model/Ticket.scala index c63de5b..47c82a7 100644 --- a/modules/model/src/main/scala/gs/smolban/model/Ticket.scala +++ b/modules/model/src/main/scala/gs/smolban/model/Ticket.scala @@ -2,6 +2,27 @@ package gs.smolban.model import cats.Show +/** Tickets represent some tracked work. + * + * @param id + * Unique identifier _within_ the [[Group]]. + * @param group + * Unique identifier of the [[Group]] that owns this ticket. + * @param createdAt + * The instant at which this ticket was created. + * @param createdBy + * The unique identifier of the [[User]] who created this ticket. + * @param title + * Arbitrary string title of the ticket. + * @param description + * Markdown contents of the ticket. + * @param tags + * List of [[Tag]] applied to this ticket. + * @param status + * Current [[Status]] of this ticket. + * @param statusHistory + * Linear history of this ticket in terms of status changes. + */ case class Ticket( id: Ticket.Id, group: Group.Id, diff --git a/modules/model/src/main/scala/gs/smolban/model/users/Role.scala b/modules/model/src/main/scala/gs/smolban/model/users/Role.scala new file mode 100644 index 0000000..afb9699 --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/users/Role.scala @@ -0,0 +1,104 @@ +package gs.smolban.model.users + +/** Roles define what each [[User]] is allowed to do. + * + * @param name + * The name of the role. + * @param scope + * The [[Scope]] to which the role applies. + */ +sealed abstract class Role( + val name: String, + val scope: Scope +): + + override def toString(): String = + s"[$name]${Role.Delimiter}$scope" + +object Role: + + val Delimiter: String = ":" + + /** Administrator for the API. Can perform any operation through the API. + */ + case object ApiAdmin extends Role(s"api${Delimiter}admin", Scope.ApiGlobal) + + /** Global read access to the API. + */ + case object ApiGlobalReader + extends Role(s"api${Delimiter}reader", Scope.ApiGlobal) + + /** Global write access to the API. + */ + case object ApiGlobalWriter + extends Role(s"api${Delimiter}writer", Scope.ApiGlobal) + + /** Administrator for a specific [[gs.smolban.model.Group]] via the API. Can + * perform any operation through the API for the indicated group. + * + * @param groupId + * The unique identifier for the group to which this role is scoped. + */ + case class ApiGroupAdmin( + groupId: gs.smolban.model.Group.Id + ) extends Role(s"api${Delimiter}group_admin", Scope.ApiGroup(groupId)) + + /** Grants API read access for a specific [[gs.smolban.model.Group]]. + * + * @param groupId + * The unique identifier for the group to which this role is scoped. + */ + case class ApiGroupReader( + groupId: gs.smolban.model.Group.Id + ) extends Role(s"api${Delimiter}group_reader", Scope.ApiGroup(groupId)) + + /** Grants API write access for a specific [[gs.smolban.model.Group]]. + * + * @param groupId + * The unique identifier for the group to which this role is scoped. + */ + case class ApiGroupWriter( + groupId: gs.smolban.model.Group.Id + ) extends Role(s"api${Delimiter}group_writer", Scope.ApiGroup(groupId)) + + /** Administrator for the UI. Can perform any operation through the UI. + */ + case object UiAdmin extends Role(s"ui${Delimiter}admin", Scope.UiGlobal) + + /** Global read access to the UI. + */ + case object UiGlobalReader extends Role("ui:reader", Scope.UiGlobal) + + /** Global write access to the UI. + */ + case object UiGlobalWriter extends Role("ui:writer", Scope.UiGlobal) + + /** Administrator for a specific [[gs.smolban.model.Group]] via the UI. Can + * perform any operation through the UI for the indicated group. + * + * @param groupId + * The unique identifier for the group to which this role is scoped. + */ + case class UiGroupAdmin( + groupId: gs.smolban.model.Group.Id + ) extends Role(s"ui${Delimiter}group_admin", Scope.UiGroup(groupId)) + + /** Grants UI read access for a specific [[gs.smolban.model.Group]]. + * + * @param groupId + * The unique identifier for the group to which this role is scoped. + */ + case class UiGroupReader( + groupId: gs.smolban.model.Group.Id + ) extends Role(s"ui${Delimiter}group_reader", Scope.UiGroup(groupId)) + + /** Grants UI write access for a specific [[gs.smolban.model.Group]]. + * + * @param groupId + * The unique identifier for the group to which this role is scoped. + */ + case class UiGroupWriter( + groupId: gs.smolban.model.Group.Id + ) extends Role(s"ui${Delimiter}group_writer", Scope.UiGroup(groupId)) + +end Role diff --git a/modules/model/src/main/scala/gs/smolban/model/users/Scope.scala b/modules/model/src/main/scala/gs/smolban/model/users/Scope.scala new file mode 100644 index 0000000..3ff5bc9 --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/users/Scope.scala @@ -0,0 +1,78 @@ +package gs.smolban.model.users + +import cats.syntax.all.* + +/** Describes a _Scope_, or service of Smolban, to which a user can have access. + * + * Scopes consist of two parts: + * + * - `service`: The name of a service Smolban offers. + * - `name`: The name of the scope within the given service. + * + * @param service + * The name of a service Smolban offers (e.g. `api`, `ui`) + * @param name + * The name of the scope within the service. + */ +sealed abstract class Scope( + val service: String, + val name: String +) + +object Scope: + + given CanEqual[Scope, Scope] = CanEqual.derived + + object Services: + + val Api: String = "api" + val Ui: String = "ui" + + end Services + + object Names: + + val Global: String = "global" + val Group: String = "group" + + end Names + + val Delimiter: String = ":" + + /** The `api:global` scope covers the Smolban API across all groups. + */ + case object ApiGlobal extends Scope(Services.Api, Names.Global): + override def toString(): String = s"[$service$Delimiter$name]" + + /** The `api:group:` scope refers the the Smolban API for a specific + * [[Group]]. + * + * @param groupId + * The unique identifier of the [[Group]]. + */ + case class ApiGroup( + groupId: gs.smolban.model.Group.Id + ) extends Scope(Services.Api, Names.Group): + + override def toString(): String = + s"[$service$Delimiter$name$Delimiter${groupId.show}]" + + /** The `ui:global` scope covers the Smolban UI across all groups. + */ + case object UiGlobal extends Scope(Services.Ui, Names.Global): + override def toString(): String = s"[$service$Delimiter$name]" + + /** The `ui:group:` scope refers the the Smolban UI for a specific + * [[Group]]. + * + * @param groupId + * The unique identifier of the [[Group]]. + */ + case class UiGroup( + groupId: gs.smolban.model.Group.Id + ) extends Scope(Services.Ui, Names.Group): + + override def toString(): String = + s"[$service$Delimiter$name$Delimiter${groupId.show}]" + +end Scope diff --git a/modules/model/src/main/scala/gs/smolban/model/users/User.scala b/modules/model/src/main/scala/gs/smolban/model/users/User.scala index ab2073d..24935b9 100644 --- a/modules/model/src/main/scala/gs/smolban/model/users/User.scala +++ b/modules/model/src/main/scala/gs/smolban/model/users/User.scala @@ -4,10 +4,25 @@ import cats.Show import gs.smolban.model.CreatedAt import gs.uuid.v0.UUID +/** Represents a user that can interact with Smolban. + * + * @param id + * Unique identifier of this user. + * @param createdAt + * The instant at which this user was created. + * @param username + * The (unique) [[Username]] of this user. + * @param userType + * The [[UserType]] of this user. + * @param roles + * List of [[Role]] assigned to this user. + */ case class User( id: User.Id, createdAt: CreatedAt, - username: Username + username: Username, + userType: UserType, + roles: List[Role] ) object User: diff --git a/modules/model/src/main/scala/gs/smolban/model/users/UserType.scala b/modules/model/src/main/scala/gs/smolban/model/users/UserType.scala new file mode 100644 index 0000000..ec3c8a6 --- /dev/null +++ b/modules/model/src/main/scala/gs/smolban/model/users/UserType.scala @@ -0,0 +1,19 @@ +package gs.smolban.model.users + +/** Enumeration that describes the type of a [[User]]. + */ +sealed abstract class UserType(val name: String) + +object UserType: + + /** Regular users are typically human, and are expected to be using the Web UI + * and _possibly_ APIs. + */ + case object Regular extends UserType("regular") + + /** Service users are intended for use by API-consuming services. They are not + * suitable for human/UI interactive use. + */ + case object Service extends UserType("service") + +end UserType