diff --git a/Integrations/MCP/ScriptBee.MCP/Generated/GatewayApi.g.cs b/Integrations/MCP/ScriptBee.MCP/Generated/GatewayApi.g.cs index d3214124..ee1d4536 100644 --- a/Integrations/MCP/ScriptBee.MCP/Generated/GatewayApi.g.cs +++ b/Integrations/MCP/ScriptBee.MCP/Generated/GatewayApi.g.cs @@ -84,79 +84,6 @@ public partial interface IGatewayApi [Get("/api/projects/{projectId}")] Task ProjectsGet(string projectId, CancellationToken cancellationToken = default); - /// Get project permissions - /// Get all the permissions for the user associated for the project. - /// projectId parameter - /// The cancellation token to cancel the request. - /// OK - /// Thrown when the request returns a non-success status code. - [Headers("Accept: application/json")] - [Get("/api/projects/{projectId}/permissions")] - Task Permissions(string projectId, CancellationToken cancellationToken = default); - - /// Get project members - /// Returns all users and groups with their roles for the project. - /// projectId parameter - /// The cancellation token to cancel the request. - /// OK - /// Thrown when the request returns a non-success status code. - [Headers("Accept: application/json")] - [Get("/api/projects/{projectId}/members")] - Task MembersGet(string projectId, CancellationToken cancellationToken = default); - - /// Update project member role - /// Assigns or updates the role for a user or group on the project. - /// projectId parameter - /// memberId parameter - /// body parameter - /// The cancellation token to cancel the request. - /// A that completes when the request is finished. - /// - /// Thrown when the request returns a non-success status code: - /// - /// - /// Status - /// Description - /// - /// - /// 400 - /// Bad Request - /// - /// - /// - [Headers("Accept: application/problem+json", "Content-Type: application/json")] - [Put("/api/projects/{projectId}/members/{memberId}")] - Task MembersPut(string projectId, string memberId, [Body] UpdateProjectMemberCommand body, CancellationToken cancellationToken = default); - - /// Remove project member - /// Removes a user or group's access from the project. - /// projectId parameter - /// memberId parameter - /// memberType parameter - /// The cancellation token to cancel the request. - /// A that completes when the request is finished. - /// Thrown when the request returns a non-success status code. - [Delete("/api/projects/{projectId}/members/{memberId}")] - Task MembersDelete(string projectId, string memberId, [Query] string memberType, CancellationToken cancellationToken = default); - - /// Get all users - /// Returns all registered users for search-ahead. - /// The cancellation token to cancel the request. - /// OK - /// Thrown when the request returns a non-success status code. - [Headers("Accept: application/json")] - [Get("/api/users")] - Task Users(CancellationToken cancellationToken = default); - - /// Get available roles - /// Returns all roles available for assignment, as defined in the authorization system. - /// The cancellation token to cancel the request. - /// OK - /// Thrown when the request returns a non-success status code. - [Headers("Accept: application/json")] - [Get("/api/roles")] - Task Roles(CancellationToken cancellationToken = default); - /// Create a new script /// Creates a new script within the specified project. /// projectId parameter @@ -358,6 +285,122 @@ public partial interface IGatewayApi [Get("/api/projects/{projectId}/structure/available-script-types")] Task AvailableScriptTypes(string projectId, CancellationToken cancellationToken = default); + /// Get project permissions + /// Get all the permissions for the user associated for the project. + /// projectId parameter + /// The cancellation token to cancel the request. + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: application/json")] + [Get("/api/projects/{projectId}/permissions")] + Task Permissions(string projectId, CancellationToken cancellationToken = default); + + /// Get project members + /// Returns all users and groups with their roles for the project. + /// projectId parameter + /// The cancellation token to cancel the request. + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: application/json")] + [Get("/api/projects/{projectId}/members")] + Task MembersGet(string projectId, CancellationToken cancellationToken = default); + + /// Update project member role + /// Assigns or updates the role for a user or group on the project. + /// projectId parameter + /// memberId parameter + /// body parameter + /// The cancellation token to cancel the request. + /// A that completes when the request is finished. + /// + /// Thrown when the request returns a non-success status code: + /// + /// + /// Status + /// Description + /// + /// + /// 400 + /// Bad Request + /// + /// + /// + [Headers("Accept: application/problem+json", "Content-Type: application/json")] + [Put("/api/projects/{projectId}/members/{memberId}")] + Task MembersPut(string projectId, string memberId, [Body] UpdateProjectMemberCommand body, CancellationToken cancellationToken = default); + + /// Remove project member + /// Removes a user or group's access from the project. + /// projectId parameter + /// memberId parameter + /// memberType parameter + /// The cancellation token to cancel the request. + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Delete("/api/projects/{projectId}/members/{memberId}")] + Task MembersDelete(string projectId, string memberId, [Query] string memberType, CancellationToken cancellationToken = default); + + /// Get all users + /// Returns all registered users for search-ahead. + /// The cancellation token to cancel the request. + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: application/json")] + [Get("/api/users")] + Task Users(CancellationToken cancellationToken = default); + + /// Get available roles + /// Returns all roles available for assignment, as defined in the authorization system. + /// The cancellation token to cancel the request. + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: application/json")] + [Get("/api/roles")] + Task Roles(CancellationToken cancellationToken = default); + + /// Get project tokens + /// Returns all the tokens associated with the project tokens. + /// projectId parameter + /// The cancellation token to cancel the request. + /// OK + /// Thrown when the request returns a non-success status code. + [Headers("Accept: application/json")] + [Get("/api/projects/{projectId}/tokens")] + Task TokensGet(string projectId, CancellationToken cancellationToken = default); + + /// Create project token + /// Creates a new project token + /// projectId parameter + /// body parameter + /// The cancellation token to cancel the request. + /// Created + /// + /// Thrown when the request returns a non-success status code: + /// + /// + /// Status + /// Description + /// + /// + /// 400 + /// Bad Request + /// + /// + /// + [Headers("Accept: application/json, application/problem+json", "Content-Type: application/json")] + [Post("/api/projects/{projectId}/tokens")] + Task TokensPost(string projectId, [Body] CreateProjectTokenRequest body, CancellationToken cancellationToken = default); + + /// Remove project token + /// Removes a token from the project. + /// projectId parameter + /// tokenId parameter + /// The cancellation token to cancel the request. + /// A that completes when the request is finished. + /// Thrown when the request returns a non-success status code. + [Delete("/api/projects/{projectId}/tokens/{tokenId}")] + Task TokensDelete(string projectId, string tokenId, CancellationToken cancellationToken = default); + /// Get all available plugins /// Retrieves a list of all available plugins from the marketplace. /// The cancellation token to cancel the request. @@ -1540,6 +1583,73 @@ public IDictionary AdditionalProperties } + /// + /// Command used to create a new project toke. + /// + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CreateProjectTokenRequest + { + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("role")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Role { get; set; } + + [JsonPropertyName("expiresAt")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.DateTimeOffset ExpiresAt { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CreateProjectTokenResponse + { + + [JsonPropertyName("id")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Id { get; set; } + + [JsonPropertyName("token")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Token { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("role")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Role { get; set; } + + [JsonPropertyName("createdAt")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("expiresAt")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.DateTimeOffset ExpiresAt { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] public partial class CreateScriptCommand { @@ -1944,6 +2054,25 @@ public IDictionary AdditionalProperties } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class GetProjectTokensResponse + { + + [JsonPropertyName("tokens")] + [System.ComponentModel.DataAnnotations.Required] + public ICollection Tokens { get; set; } = new System.Collections.ObjectModel.Collection(); + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] public partial class GetScriptDataResponse { @@ -2739,6 +2868,40 @@ public IDictionary AdditionalProperties } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ProjectToken + { + + [JsonPropertyName("id")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Id { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("role")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public string Role { get; set; } + + [JsonPropertyName("createdAt")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("expiresAt")] + [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)] + public System.DateTimeOffset ExpiresAt { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.7.1.0 (NJsonSchema v11.6.1.0 (Newtonsoft.Json v13.0.0.0))")] public partial class RoleInfo { diff --git a/ScriptBeeWebApp/src/Common/Common/DateTimeProvider.cs b/ScriptBeeWebApp/src/Common/Common/DateTimeProvider.cs index d0d83e50..c115c030 100644 --- a/ScriptBeeWebApp/src/Common/Common/DateTimeProvider.cs +++ b/ScriptBeeWebApp/src/Common/Common/DateTimeProvider.cs @@ -1,6 +1,6 @@ namespace ScriptBee.Common; -public class DateTimeProvider : IDateTimeProvider +public sealed class DateTimeProvider : IDateTimeProvider { public DateTimeOffset UtcNow() => DateTimeOffset.UtcNow; } diff --git a/ScriptBeeWebApp/src/Common/Common/GuidProvider.cs b/ScriptBeeWebApp/src/Common/Common/GuidProvider.cs index df34a31a..5735e242 100644 --- a/ScriptBeeWebApp/src/Common/Common/GuidProvider.cs +++ b/ScriptBeeWebApp/src/Common/Common/GuidProvider.cs @@ -1,6 +1,6 @@ namespace ScriptBee.Common; -public class GuidProvider : IGuidProvider +public sealed class GuidProvider : IGuidProvider { public Guid NewGuid() => Guid.CreateVersion7(); } diff --git a/ScriptBeeWebApp/src/Common/Common/ISecureRandomProvider.cs b/ScriptBeeWebApp/src/Common/Common/ISecureRandomProvider.cs new file mode 100644 index 00000000..3026cb0c --- /dev/null +++ b/ScriptBeeWebApp/src/Common/Common/ISecureRandomProvider.cs @@ -0,0 +1,6 @@ +namespace ScriptBee.Common; + +public interface ISecureRandomProvider +{ + byte[] GetBytes(int count); +} diff --git a/ScriptBeeWebApp/src/Common/Common/SecureRandomProvider.cs b/ScriptBeeWebApp/src/Common/Common/SecureRandomProvider.cs new file mode 100644 index 00000000..7521c7b2 --- /dev/null +++ b/ScriptBeeWebApp/src/Common/Common/SecureRandomProvider.cs @@ -0,0 +1,8 @@ +using System.Security.Cryptography; + +namespace ScriptBee.Common; + +public sealed class SecureRandomProvider : ISecureRandomProvider +{ + public byte[] GetBytes(int count) => RandomNumberGenerator.GetBytes(32); +} diff --git a/ScriptBeeWebApp/src/Common/Model/Project/ProjectToken.cs b/ScriptBeeWebApp/src/Common/Model/Project/ProjectToken.cs new file mode 100644 index 00000000..d54a0a9e --- /dev/null +++ b/ScriptBeeWebApp/src/Common/Model/Project/ProjectToken.cs @@ -0,0 +1,13 @@ +using ScriptBee.Domain.Model.User; + +namespace ScriptBee.Domain.Model.Project; + +public record ProjectToken( + ProjectTokenId Id, + ProjectId ProjectId, + string TokenHash, + string? Description, + UserRole Role, + DateTimeOffset CreatedAt, + DateTimeOffset ExpiresAt +); diff --git a/ScriptBeeWebApp/src/Common/Model/Project/ProjectTokenId.cs b/ScriptBeeWebApp/src/Common/Model/Project/ProjectTokenId.cs new file mode 100644 index 00000000..5adb40c1 --- /dev/null +++ b/ScriptBeeWebApp/src/Common/Model/Project/ProjectTokenId.cs @@ -0,0 +1,6 @@ +namespace ScriptBee.Domain.Model.Project; + +public readonly record struct ProjectTokenId(string Value) +{ + public override string ToString() => Value; +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/Entity/MongodbProjectToken.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/Entity/MongodbProjectToken.cs new file mode 100644 index 00000000..da196e8d --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/Entity/MongodbProjectToken.cs @@ -0,0 +1,37 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using ScriptBee.Domain.Model.Project; +using ScriptBee.Domain.Model.User; +using ScriptBee.Persistence.Mongodb.Repository; + +namespace ScriptBee.Persistence.Mongodb.Entity; + +[BsonIgnoreExtraElements] +public class MongodbProjectToken : IDocument +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } = null!; + + public required string ProjectId { get; init; } + public required string TokenHash { get; init; } + public string? Description { get; init; } + + public required string Role { get; init; } + + public required DateTimeOffset CreatedAt { get; init; } + public required DateTimeOffset ExpiresAt { get; init; } + + public ProjectToken ToProjectToken() + { + return new ProjectToken( + new ProjectTokenId(Id), + Domain.Model.Project.ProjectId.FromValue(ProjectId), + TokenHash, + Description, + new UserRole(Role), + CreatedAt, + ExpiresAt + ); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/Extensions/GatewayMongoDbExtensions.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/Extensions/GatewayMongoDbExtensions.cs index f9f54530..4dfcb70d 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/Extensions/GatewayMongoDbExtensions.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/Extensions/GatewayMongoDbExtensions.cs @@ -43,7 +43,7 @@ public IServiceCollection AddResourceMembersAdapters(IMongoDatabase mongoDatabas .AddSingleton(); } - public IServiceCollection AddUserManagementAdaptersAdapters(IMongoDatabase mongoDatabase) + public IServiceCollection AddUserManagementAdapters(IMongoDatabase mongoDatabase) { return services .AddMongoCollection(mongoDatabase, "Users") @@ -52,5 +52,14 @@ public IServiceCollection AddUserManagementAdaptersAdapters(IMongoDatabase mongo .AddSingleton() .AddSingleton(); } + + public IServiceCollection AddProjectTokensAdapters(IMongoDatabase mongoDatabase) + { + return services + .AddMongoCollection(mongoDatabase, "ProjectTokens") + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } } } diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/ProjectTokensPersistenceAdapter.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/ProjectTokensPersistenceAdapter.cs new file mode 100644 index 00000000..ad9cfca2 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Persistence.Mongodb.Gateway/ProjectTokensPersistenceAdapter.cs @@ -0,0 +1,63 @@ +using MongoDB.Driver; +using ScriptBee.Domain.Model.Project; +using ScriptBee.Domain.Model.User; +using ScriptBee.Persistence.Mongodb.Entity; +using ScriptBee.Persistence.Mongodb.Repository; +using ScriptBee.Ports.Permissions; + +namespace ScriptBee.Persistence.Mongodb; + +public sealed class ProjectTokensPersistenceAdapter( + IMongoRepository mongoRepository +) : ICreateProjectToken, IGetAllProjectTokens, IDeleteProjectToken +{ + public async Task CreateToken( + ProjectId projectId, + string tokenHash, + string? description, + UserRole role, + DateTimeOffset expiresAt, + CancellationToken cancellationToken + ) + { + var mongoToken = new MongodbProjectToken + { + ProjectId = projectId.Value, + Description = description, + TokenHash = tokenHash, + Role = role.Value, + CreatedAt = DateTimeOffset.UtcNow, + ExpiresAt = expiresAt, + }; + + await mongoRepository.CreateDocument(mongoToken, cancellationToken); + + return mongoToken.ToProjectToken(); + } + + public async Task> GetAllForProjectId( + ProjectId projectId, + CancellationToken cancellationToken + ) + { + var tokens = await mongoRepository.GetAllDocuments( + token => token.ProjectId == projectId.Value, + cancellationToken + ); + return [.. tokens.Select(t => t.ToProjectToken())]; + } + + public async Task DeleteToken( + ProjectId projectId, + ProjectTokenId tokenId, + CancellationToken cancellationToken + ) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(m => m.ProjectId, projectId.Value), + Builders.Filter.Eq(m => m.Id, tokenId.Value) + ); + + await mongoRepository.MongoCollection.DeleteOneAsync(filter, cancellationToken); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebProjectMember.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebProjectMember.cs deleted file mode 100644 index 9af39149..00000000 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebProjectMember.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts; - -public record WebProjectMember(string MemberId, string MemberType, string Role); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebRoleInfo.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebRoleInfo.cs deleted file mode 100644 index 9acfa7ce..00000000 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebRoleInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts; - -public record WebRoleInfo(string Id, string Description); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebUpdateProjectMemberCommand.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebUpdateProjectMemberCommand.cs deleted file mode 100644 index e905c8d8..00000000 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebUpdateProjectMemberCommand.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FluentValidation; - -namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts; - -public record WebUpdateProjectMemberCommand(string Role, string MemberType); - -public class WebUpdateProjectMemberCommandValidator - : AbstractValidator -{ - public WebUpdateProjectMemberCommandValidator() - { - RuleFor(x => x.Role).NotEmpty(); - RuleFor(x => x.MemberType) - .NotEmpty() - .Must(t => t == "user" || t == "group") - .WithMessage("MemberType must be 'user' or 'group'."); - } -} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebUserInfo.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebUserInfo.cs deleted file mode 100644 index e471a4fc..00000000 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebUserInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts; - -public record WebUserInfo(string Id, string Name); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebCreateProjectTokenRequest.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebCreateProjectTokenRequest.cs new file mode 100644 index 00000000..72b9d009 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebCreateProjectTokenRequest.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; +using ScriptBee.Domain.Model.Project; +using ScriptBee.Domain.Model.User; +using ScriptBee.UseCases.Gateway; + +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +[Description("Command used to create a new project toke.")] +public record WebCreateProjectTokenRequest( + string? Description, + string Role, + DateTimeOffset ExpiresAt +) +{ + public CreateProjectTokenCommand Map(ProjectId projectId) + { + return new CreateProjectTokenCommand(projectId, Description, new UserRole(Role), ExpiresAt); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebCreateProjectTokenResponse.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebCreateProjectTokenResponse.cs new file mode 100644 index 00000000..82fd78a5 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebCreateProjectTokenResponse.cs @@ -0,0 +1,25 @@ +using ScriptBee.UseCases.Gateway; + +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +public record WebCreateProjectTokenResponse( + string Id, + string Token, + string? Description, + string Role, + DateTimeOffset CreatedAt, + DateTimeOffset ExpiresAt +) +{ + public static WebCreateProjectTokenResponse Map(NewProjectTokenResult result) + { + return new WebCreateProjectTokenResponse( + result.Token.Id.Value, + result.UnhashedToken, + result.Token.Description, + result.Token.Role.Value, + result.Token.CreatedAt, + result.Token.ExpiresAt + ); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetAllUsersResponse.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetAllUsersResponse.cs similarity index 50% rename from ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetAllUsersResponse.cs rename to ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetAllUsersResponse.cs index 29afab3c..3dcedbd4 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetAllUsersResponse.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetAllUsersResponse.cs @@ -1,3 +1,3 @@ -namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts; +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; public record WebGetAllUsersResponse(IEnumerable Users); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetAvailableRolesResponse.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetAvailableRolesResponse.cs similarity index 52% rename from ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetAvailableRolesResponse.cs rename to ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetAvailableRolesResponse.cs index e0d714f6..bd14774f 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetAvailableRolesResponse.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetAvailableRolesResponse.cs @@ -1,3 +1,3 @@ -namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts; +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; public record WebGetAvailableRolesResponse(IEnumerable Roles); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetProjectMembersResponse.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetProjectMembersResponse.cs similarity index 54% rename from ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetProjectMembersResponse.cs rename to ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetProjectMembersResponse.cs index 114ded74..2e8cd8f1 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetProjectMembersResponse.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetProjectMembersResponse.cs @@ -1,3 +1,3 @@ -namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts; +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; public record WebGetProjectMembersResponse(IEnumerable Members); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetProjectTokensResponse.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetProjectTokensResponse.cs new file mode 100644 index 00000000..7f11e3aa --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetProjectTokensResponse.cs @@ -0,0 +1,3 @@ +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +public record WebGetProjectTokensResponse(IEnumerable Tokens); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetUserPermissionsForProjectResponse.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetUserPermissionsForProjectResponse.cs similarity index 62% rename from ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetUserPermissionsForProjectResponse.cs rename to ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetUserPermissionsForProjectResponse.cs index faa60c0b..1ef32abd 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/Contracts/WebGetUserPermissionsForProjectResponse.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebGetUserPermissionsForProjectResponse.cs @@ -1,4 +1,4 @@ -namespace ScriptBee.Web.EndpointDefinitions.Project.Contracts; +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; public record WebGetUserPermissionsForProjectResponse( string? Role, diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebProjectMember.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebProjectMember.cs new file mode 100644 index 00000000..e4f7eeea --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebProjectMember.cs @@ -0,0 +1,11 @@ +using ScriptBee.Domain.Model.User; + +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +public record WebProjectMember(string MemberId, string MemberType, string Role) +{ + public static WebProjectMember Map(ProjectMember member) + { + return new WebProjectMember(member.MemberId, member.MemberType, member.Role.Value); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebProjectToken.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebProjectToken.cs new file mode 100644 index 00000000..fca2c395 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebProjectToken.cs @@ -0,0 +1,23 @@ +using ScriptBee.Domain.Model.Project; + +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +public record WebProjectToken( + string Id, + string? Description, + string Role, + DateTimeOffset CreatedAt, + DateTimeOffset ExpiresAt +) +{ + public static WebProjectToken Map(ProjectToken token) + { + return new WebProjectToken( + token.Id.Value, + token.Description, + token.Role.Value, + token.CreatedAt, + token.ExpiresAt + ); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebRoleInfo.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebRoleInfo.cs new file mode 100644 index 00000000..4df66118 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebRoleInfo.cs @@ -0,0 +1,3 @@ +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +public record WebRoleInfo(string Id, string Description); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebUpdateProjectMemberCommand.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebUpdateProjectMemberCommand.cs new file mode 100644 index 00000000..70d4920d --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebUpdateProjectMemberCommand.cs @@ -0,0 +1,3 @@ +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +public record WebUpdateProjectMemberCommand(string Role, string MemberType); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebUserInfo.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebUserInfo.cs new file mode 100644 index 00000000..e3dcda28 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Contracts/WebUserInfo.cs @@ -0,0 +1,3 @@ +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +public record WebUserInfo(string Id, string Name); diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/GetProjectPermissionsEndpoint.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/GetProjectPermissionsEndpoint.cs similarity index 93% rename from ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/GetProjectPermissionsEndpoint.cs rename to ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/GetProjectPermissionsEndpoint.cs index 66081464..0cb20eea 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/GetProjectPermissionsEndpoint.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/GetProjectPermissionsEndpoint.cs @@ -5,9 +5,9 @@ using ScriptBee.Domain.Model.Project; using ScriptBee.Service.Gateway; using ScriptBee.UseCases.Gateway; -using ScriptBee.Web.EndpointDefinitions.Project.Contracts; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; -namespace ScriptBee.Web.EndpointDefinitions.Project; +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess; public class GetProjectPermissionsEndpoint : IEndpointDefinition { diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/ManageProjectAccessEndpoint.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/ManageProjectAccessEndpoint.cs similarity index 82% rename from ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/ManageProjectAccessEndpoint.cs rename to ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/ManageProjectAccessEndpoint.cs index 34cb0e0a..e7a2e81e 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/Project/ManageProjectAccessEndpoint.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/ManageProjectAccessEndpoint.cs @@ -7,19 +7,15 @@ using ScriptBee.Domain.Model.User; using ScriptBee.Service.Gateway; using ScriptBee.UseCases.Gateway; -using ScriptBee.Web.EndpointDefinitions.Project.Contracts; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; -namespace ScriptBee.Web.EndpointDefinitions.Project; +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess; public class ManageProjectAccessEndpoint : IEndpointDefinition { public void DefineServices(IServiceCollection services) { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); } public void DefineEndpoints(IEndpointRouteBuilder app) @@ -60,7 +56,7 @@ public void DefineEndpoints(IEndpointRouteBuilder app) private static async Task> GetProjectMembers( [FromRoute] string projectId, - IGetProjectMembersUseCase useCase, + IManageUsersUseCase useCase, CancellationToken cancellationToken ) { @@ -70,9 +66,7 @@ CancellationToken cancellationToken ); return TypedResults.Ok( - new WebGetProjectMembersResponse( - members.Select(m => new WebProjectMember(m.MemberId, m.MemberType, m.Role.Value)) - ) + new WebGetProjectMembersResponse(members.Select(WebProjectMember.Map)) ); } @@ -80,7 +74,7 @@ private static async Task UpdateProjectMember( [FromRoute] string projectId, [FromRoute] string memberId, [FromBody] WebUpdateProjectMemberCommand command, - IUpdateProjectMemberUseCase useCase, + IManageUsersUseCase useCase, CancellationToken cancellationToken ) { @@ -101,7 +95,7 @@ private static async Task RemoveProjectMember( [FromRoute] string projectId, [FromRoute] string memberId, [FromQuery] string memberType, - IRemoveProjectMemberUseCase useCase, + IManageUsersUseCase useCase, CancellationToken cancellationToken ) { @@ -114,7 +108,7 @@ await useCase.RemoveProjectMember( } private static async Task> GetAllUsers( - IGetAllUsersUseCase useCase, + IManageUsersUseCase useCase, CancellationToken cancellationToken ) { @@ -126,7 +120,7 @@ CancellationToken cancellationToken } private static async Task> GetAvailableRoles( - IGetAvailableRolesUseCase useCase, + IManageUsersUseCase useCase, CancellationToken cancellationToken ) { diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/ManageProjectTokensEndpoint.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/ManageProjectTokensEndpoint.cs new file mode 100644 index 00000000..9a8cf577 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/ManageProjectTokensEndpoint.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using ScriptBee.Adapters.Auth.Extensions; +using ScriptBee.Common.Web; +using ScriptBee.Common.Web.Validation; +using ScriptBee.Domain.Model.Project; +using ScriptBee.Service.Gateway; +using ScriptBee.UseCases.Gateway; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess; + +public class ManageProjectTokensEndpoint : IEndpointDefinition +{ + public void DefineServices(IServiceCollection services) + { + services.AddSingleton(); + } + + public void DefineEndpoints(IEndpointRouteBuilder app) + { + app.MapGet("/api/projects/{projectId}/tokens", GetProjectTokens) + .WithTags("Projects", "Access") + .WithSummary("Get project tokens") + .WithDescription("Returns all the tokens associated with the project tokens.") + .RequireAction("project:manage_access"); + + app.MapPost("/api/projects/{projectId}/tokens", CreateProjectToken) + .WithTags("Projects", "Access") + .WithSummary("Create project token") + .WithDescription("Creates a new project token") + .WithRequestValidation() + .RequireAction("token:create"); + + app.MapDelete("/api/projects/{projectId}/tokens/{tokenId}", RemoveProjectToken) + .WithTags("Projects", "Access") + .WithSummary("Remove project token") + .WithDescription("Removes a token from the project.") + .RequireAction("token:delete"); + } + + private static async Task> GetProjectTokens( + [FromRoute] string projectId, + IManageProjectTokensUseCase useCase, + CancellationToken cancellationToken + ) + { + var members = await useCase.GetProjectTokens( + ProjectId.FromValue(projectId), + cancellationToken + ); + + return TypedResults.Ok( + new WebGetProjectTokensResponse(members.Select(WebProjectToken.Map)) + ); + } + + private static async Task> CreateProjectToken( + [FromRoute] string projectId, + [FromBody] WebCreateProjectTokenRequest command, + IManageProjectTokensUseCase useCase, + CancellationToken cancellationToken + ) + { + var result = await useCase.CreateProjectToken( + command.Map(ProjectId.FromValue(projectId)), + cancellationToken + ); + + return TypedResults.Created( + $"/api/projects/{projectId}/tokens/{result.Token.Id.Value}", + WebCreateProjectTokenResponse.Map(result) + ); + } + + private static async Task RemoveProjectToken( + [FromRoute] string projectId, + [FromRoute] string tokenId, + IManageProjectTokensUseCase useCase, + CancellationToken cancellationToken + ) + { + await useCase.DeleteProjectToken( + ProjectId.FromValue(projectId), + new ProjectTokenId(tokenId), + cancellationToken + ); + + return TypedResults.NoContent(); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Validation/CreateProjectTokenValidator.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Validation/CreateProjectTokenValidator.cs new file mode 100644 index 00000000..d62d41d8 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Validation/CreateProjectTokenValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Validation; + +public class CreateProjectTokenValidator : AbstractValidator +{ + public CreateProjectTokenValidator() + { + RuleFor(x => x.ExpiresAt).NotNull(); + RuleFor(x => x.Role).NotEmpty(); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Validation/UpdateProjectMemberCommandValidator.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Validation/UpdateProjectMemberCommandValidator.cs new file mode 100644 index 00000000..30c165c5 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/EndpointDefinitions/ProjectAccess/Validation/UpdateProjectMemberCommandValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +namespace ScriptBee.Web.EndpointDefinitions.ProjectAccess.Validation; + +public class UpdateProjectMemberCommandValidator : AbstractValidator +{ + public UpdateProjectMemberCommandValidator() + { + RuleFor(x => x.Role).NotEmpty(); + RuleFor(x => x.MemberType) + .NotEmpty() + .Must(t => t is "user" or "group") + .WithMessage("Member Type must be 'user' or 'group'."); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/CommonServicesExtensions.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/CommonServicesExtensions.cs index daad53ef..59126671 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/CommonServicesExtensions.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/CommonServicesExtensions.cs @@ -11,6 +11,7 @@ public static IServiceCollection AddCommonServices(this IServiceCollection servi return services .AddSingleton() .AddSingleton() + .AddSingleton() .AddScoped() .AddSingleton(); } diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/MongoDbExtensions.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/MongoDbExtensions.cs index 17b05d21..af898bcc 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/MongoDbExtensions.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/MongoDbExtensions.cs @@ -18,7 +18,8 @@ public static IServiceCollection AddMongoDb( .AddProjectAdapters(mongoDatabase) .AddProjectInstancesAdapters(mongoDatabase) .AddResourceMembersAdapters(mongoDatabase) - .AddUserManagementAdaptersAdapters(mongoDatabase) + .AddUserManagementAdapters(mongoDatabase) + .AddProjectTokensAdapters(mongoDatabase) .AddAnalysisAdapters(mongoDatabase) .AddScriptAdapters(mongoDatabase); } diff --git a/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/UserManagementExtensions.cs b/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/UserManagementExtensions.cs index 8ff898a7..88211154 100644 --- a/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/UserManagementExtensions.cs +++ b/ScriptBeeWebApp/src/Gateway/Adapters/Web/Extensions/UserManagementExtensions.cs @@ -1,6 +1,4 @@ -using ScriptBee.Service.Gateway; using ScriptBee.Service.Gateway.Config; -using ScriptBee.UseCases.Gateway; namespace ScriptBee.Web.Extensions; @@ -15,6 +13,6 @@ string userManagementConfiguration .AddOptions() .BindConfiguration(userManagementConfiguration); - return services.AddMemoryCache().AddSingleton(); + return services.AddMemoryCache(); } } diff --git a/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/ICreateProjectToken.cs b/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/ICreateProjectToken.cs new file mode 100644 index 00000000..1c1229f2 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/ICreateProjectToken.cs @@ -0,0 +1,16 @@ +using ScriptBee.Domain.Model.Project; +using ScriptBee.Domain.Model.User; + +namespace ScriptBee.Ports.Permissions; + +public interface ICreateProjectToken +{ + Task CreateToken( + ProjectId projectId, + string tokenHash, + string? description, + UserRole role, + DateTimeOffset expiresAt, + CancellationToken cancellationToken + ); +} diff --git a/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/IDeleteProjectToken.cs b/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/IDeleteProjectToken.cs new file mode 100644 index 00000000..226d4d53 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/IDeleteProjectToken.cs @@ -0,0 +1,12 @@ +using ScriptBee.Domain.Model.Project; + +namespace ScriptBee.Ports.Permissions; + +public interface IDeleteProjectToken +{ + Task DeleteToken( + ProjectId projectId, + ProjectTokenId tokenId, + CancellationToken cancellationToken + ); +} diff --git a/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/IGetAllProjectTokens.cs b/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/IGetAllProjectTokens.cs new file mode 100644 index 00000000..194fad88 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Application/Ports.Permissions/IGetAllProjectTokens.cs @@ -0,0 +1,11 @@ +using ScriptBee.Domain.Model.Project; + +namespace ScriptBee.Ports.Permissions; + +public interface IGetAllProjectTokens +{ + Task> GetAllForProjectId( + ProjectId projectId, + CancellationToken cancellationToken + ); +} diff --git a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetAllUsersService.cs b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetAllUsersService.cs deleted file mode 100644 index 8a6f2b54..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetAllUsersService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using ScriptBee.Domain.Model.User; -using ScriptBee.Ports.Permissions; -using ScriptBee.UseCases.Gateway; - -namespace ScriptBee.Service.Gateway; - -public sealed class GetAllUsersService(IGetAllUsers getAllUsers) : IGetAllUsersUseCase -{ - public Task> GetAllUsers(CancellationToken cancellationToken) - { - return getAllUsers.GetAllUsers(cancellationToken); - } -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetAvailableRolesService.cs b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetAvailableRolesService.cs deleted file mode 100644 index bebd4a32..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetAvailableRolesService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ScriptBee.Domain.Model.User; -using ScriptBee.Ports.Permissions; -using ScriptBee.UseCases.Gateway; - -namespace ScriptBee.Service.Gateway; - -public sealed class GetAvailableRolesService(IGetAvailableRoles getAvailableRoles) - : IGetAvailableRolesUseCase -{ - public Task> GetAvailableRoles(CancellationToken cancellationToken) - { - return getAvailableRoles.GetRoles(cancellationToken); - } -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetProjectMembersService.cs b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetProjectMembersService.cs deleted file mode 100644 index 98d720a9..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/GetProjectMembersService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using ScriptBee.Domain.Model.Project; -using ScriptBee.Domain.Model.User; -using ScriptBee.Ports.Permissions; -using ScriptBee.UseCases.Gateway; - -namespace ScriptBee.Service.Gateway; - -public sealed class GetProjectMembersService(IGetProjectMembers getProjectMembers) - : IGetProjectMembersUseCase -{ - public Task> GetProjectMembers( - ProjectId projectId, - CancellationToken cancellationToken - ) - { - return getProjectMembers.GetProjectMembers(projectId, cancellationToken); - } -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/ManageProjectTokensService.cs b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/ManageProjectTokensService.cs new file mode 100644 index 00000000..fab2c6df --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/ManageProjectTokensService.cs @@ -0,0 +1,80 @@ +using System.Security.Cryptography; +using System.Text; +using ScriptBee.Common; +using ScriptBee.Domain.Model.Project; +using ScriptBee.Ports.Permissions; +using ScriptBee.UseCases.Gateway; + +namespace ScriptBee.Service.Gateway; + +public sealed class ManageProjectTokensService( + ISecureRandomProvider secureRandomProvider, + IGetAllProjectTokens getAllProjectTokens, + ICreateProjectToken createProjectToken, + IDeleteProjectToken deleteProjectToken +) : IManageProjectTokensUseCase +{ + private const string Prefix = "sb_at_"; + private const int TokenRandomBytesSize = 32; + + public async Task> GetProjectTokens( + ProjectId projectId, + CancellationToken cancellationToken + ) + { + return await getAllProjectTokens.GetAllForProjectId(projectId, cancellationToken); + } + + public async Task CreateProjectToken( + CreateProjectTokenCommand command, + CancellationToken cancellationToken + ) + { + var (rawToken, tokenHash) = GenerateToken(); + + var token = await createProjectToken.CreateToken( + command.ProjectId, + tokenHash, + command.Description, + command.Role, + command.ExpiresAt, + cancellationToken + ); + + return new NewProjectTokenResult(token, rawToken); + } + + public async Task DeleteProjectToken( + ProjectId projectId, + ProjectTokenId id, + CancellationToken cancellationToken + ) + { + await deleteProjectToken.DeleteToken(projectId, id, cancellationToken); + } + + private (string rawToken, string tokenHash) GenerateToken() + { + var randomBytes = secureRandomProvider.GetBytes(TokenRandomBytesSize); + + var secretPayload = Convert + .ToBase64String(randomBytes) + .Replace("+", "") + .Replace("/", "") + .Replace("=", ""); + + var rawToken = $"{Prefix}{secretPayload}"; + + var tokenHash = ComputeHash(rawToken); + + return (rawToken, tokenHash); + } + + private static string ComputeHash(string input) + { + var inputBytes = Encoding.UTF8.GetBytes(input); + var hashBytes = SHA256.HashData(inputBytes); + + return Convert.ToHexString(hashBytes); + } +} diff --git a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/ManageUsersService.cs b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/ManageUsersService.cs index 1825e338..ecb0e452 100644 --- a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/ManageUsersService.cs +++ b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/ManageUsersService.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using ScriptBee.Domain.Model.Project; using ScriptBee.Domain.Model.User; using ScriptBee.Ports.Permissions; using ScriptBee.Service.Gateway.Config; @@ -10,9 +11,19 @@ namespace ScriptBee.Service.Gateway; public sealed class ManageUsersService( IOptions userManagementConfigOptions, IMemoryCache cache, - IGetOrAddUser getOrAddUser + IGetAvailableRoles getAvailableRoles, + IGetOrAddUser getOrAddUser, + IGetAllUsers getAllUsers, + IGetProjectMembers getProjectMembers, + ISetResourceRole setResourceRole, + IRemoveProjectMember removeProjectMember ) : IManageUsersUseCase { + public Task> GetAvailableRoles(CancellationToken cancellationToken) + { + return getAvailableRoles.GetRoles(cancellationToken); + } + public async Task GetUserId( string externalUserId, string externalUserName, @@ -28,6 +39,46 @@ CancellationToken cancellationToken return userId; } + public Task> GetAllUsers(CancellationToken cancellationToken) + { + return getAllUsers.GetAllUsers(cancellationToken); + } + + public Task> GetProjectMembers( + ProjectId projectId, + CancellationToken cancellationToken + ) + { + return getProjectMembers.GetProjectMembers(projectId, cancellationToken); + } + + public Task RemoveProjectMember( + RemoveProjectMemberCommand command, + CancellationToken cancellationToken + ) + { + return removeProjectMember.RemoveProjectMember( + command.ProjectId, + command.MemberId, + command.MemberType, + cancellationToken + ); + } + + public Task UpdateProjectMember( + UpdateProjectMemberCommand command, + CancellationToken cancellationToken + ) + { + return setResourceRole.SetRoleForMember( + command.MemberId, + command.MemberType, + command.ProjectId, + command.Role, + cancellationToken + ); + } + private MemoryCacheEntryOptions GetCacheOptions() { var config = userManagementConfigOptions.Value; diff --git a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/RemoveProjectMemberService.cs b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/RemoveProjectMemberService.cs deleted file mode 100644 index a7b5fcdf..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/RemoveProjectMemberService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using ScriptBee.Ports.Permissions; -using ScriptBee.UseCases.Gateway; - -namespace ScriptBee.Service.Gateway; - -public sealed class RemoveProjectMemberService(IRemoveProjectMember removeProjectMember) - : IRemoveProjectMemberUseCase -{ - public Task RemoveProjectMember( - RemoveProjectMemberCommand command, - CancellationToken cancellationToken - ) - { - return removeProjectMember.RemoveProjectMember( - command.ProjectId, - command.MemberId, - command.MemberType, - cancellationToken - ); - } -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/UpdateProjectMemberService.cs b/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/UpdateProjectMemberService.cs deleted file mode 100644 index c353b5e1..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/Service.Gateway/UpdateProjectMemberService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ScriptBee.Ports.Permissions; -using ScriptBee.UseCases.Gateway; - -namespace ScriptBee.Service.Gateway; - -public sealed class UpdateProjectMemberService(ISetResourceRole setResourceRole) - : IUpdateProjectMemberUseCase -{ - public Task UpdateProjectMember( - UpdateProjectMemberCommand command, - CancellationToken cancellationToken - ) - { - return setResourceRole.SetRoleForMember( - command.MemberId, - command.MemberType, - command.ProjectId, - command.Role, - cancellationToken - ); - } -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/CreateProjectTokenCommand.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/CreateProjectTokenCommand.cs new file mode 100644 index 00000000..6010b7c1 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/CreateProjectTokenCommand.cs @@ -0,0 +1,11 @@ +using ScriptBee.Domain.Model.Project; +using ScriptBee.Domain.Model.User; + +namespace ScriptBee.UseCases.Gateway; + +public record CreateProjectTokenCommand( + ProjectId ProjectId, + string? Description, + UserRole Role, + DateTimeOffset ExpiresAt +); diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetAllUsersUseCase.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetAllUsersUseCase.cs deleted file mode 100644 index d1596925..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetAllUsersUseCase.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ScriptBee.Domain.Model.User; - -namespace ScriptBee.UseCases.Gateway; - -public interface IGetAllUsersUseCase -{ - Task> GetAllUsers(CancellationToken cancellationToken); -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetAvailableRolesUseCase.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetAvailableRolesUseCase.cs deleted file mode 100644 index 816d729e..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetAvailableRolesUseCase.cs +++ /dev/null @@ -1,8 +0,0 @@ -using ScriptBee.Domain.Model.User; - -namespace ScriptBee.UseCases.Gateway; - -public interface IGetAvailableRolesUseCase -{ - Task> GetAvailableRoles(CancellationToken cancellationToken); -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetProjectMembersUseCase.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetProjectMembersUseCase.cs deleted file mode 100644 index 33daa607..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IGetProjectMembersUseCase.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ScriptBee.Domain.Model.Project; -using ScriptBee.Domain.Model.User; - -namespace ScriptBee.UseCases.Gateway; - -public interface IGetProjectMembersUseCase -{ - Task> GetProjectMembers( - ProjectId projectId, - CancellationToken cancellationToken - ); -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IManageProjectTokensUseCase.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IManageProjectTokensUseCase.cs new file mode 100644 index 00000000..c9583098 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IManageProjectTokensUseCase.cs @@ -0,0 +1,22 @@ +using ScriptBee.Domain.Model.Project; + +namespace ScriptBee.UseCases.Gateway; + +public interface IManageProjectTokensUseCase +{ + Task> GetProjectTokens( + ProjectId projectId, + CancellationToken cancellationToken + ); + + Task CreateProjectToken( + CreateProjectTokenCommand command, + CancellationToken cancellationToken + ); + + Task DeleteProjectToken( + ProjectId projectId, + ProjectTokenId id, + CancellationToken cancellationToken + ); +} diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IManageUsersUseCase.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IManageUsersUseCase.cs index 68200d73..67349b01 100644 --- a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IManageUsersUseCase.cs +++ b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IManageUsersUseCase.cs @@ -1,12 +1,32 @@ +using ScriptBee.Domain.Model.Project; using ScriptBee.Domain.Model.User; namespace ScriptBee.UseCases.Gateway; public interface IManageUsersUseCase { + Task> GetAvailableRoles(CancellationToken cancellationToken); + Task GetUserId( string externalUserId, string externalUserName, CancellationToken cancellationToken ); + + Task> GetAllUsers(CancellationToken cancellationToken); + + Task> GetProjectMembers( + ProjectId projectId, + CancellationToken cancellationToken + ); + + Task RemoveProjectMember( + RemoveProjectMemberCommand command, + CancellationToken cancellationToken + ); + + Task UpdateProjectMember( + UpdateProjectMemberCommand command, + CancellationToken cancellationToken + ); } diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IRemoveProjectMemberUseCase.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IRemoveProjectMemberUseCase.cs deleted file mode 100644 index 2a57f470..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IRemoveProjectMemberUseCase.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ScriptBee.UseCases.Gateway; - -public interface IRemoveProjectMemberUseCase -{ - Task RemoveProjectMember( - RemoveProjectMemberCommand command, - CancellationToken cancellationToken - ); -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IUpdateProjectMemberUseCase.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IUpdateProjectMemberUseCase.cs deleted file mode 100644 index 092dcb72..00000000 --- a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/IUpdateProjectMemberUseCase.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ScriptBee.UseCases.Gateway; - -public interface IUpdateProjectMemberUseCase -{ - Task UpdateProjectMember( - UpdateProjectMemberCommand command, - CancellationToken cancellationToken - ); -} diff --git a/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/NewProjectTokenResult.cs b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/NewProjectTokenResult.cs new file mode 100644 index 00000000..729c9063 --- /dev/null +++ b/ScriptBeeWebApp/src/Gateway/Application/UseCases.Gateway/NewProjectTokenResult.cs @@ -0,0 +1,5 @@ +using ScriptBee.Domain.Model.Project; + +namespace ScriptBee.UseCases.Gateway; + +public record NewProjectTokenResult(ProjectToken Token, string UnhashedToken); diff --git a/ScriptBeeWebApp/test/Common/Common.Tests/SecureRandomProviderTest.cs b/ScriptBeeWebApp/test/Common/Common.Tests/SecureRandomProviderTest.cs new file mode 100644 index 00000000..46a4d5f8 --- /dev/null +++ b/ScriptBeeWebApp/test/Common/Common.Tests/SecureRandomProviderTest.cs @@ -0,0 +1,14 @@ +namespace ScriptBee.Common.Tests; + +public class SecureRandomProviderTest +{ + private readonly SecureRandomProvider _provider = new(); + + [Fact] + public void GetBytes() + { + var guid = _provider.GetBytes(32); + + guid.Length.ShouldBe(32); + } +} diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Persistence.Mongodb.Gateway.Tests/ProjectTokensPersistenceAdapterTests.cs b/ScriptBeeWebApp/test/Gateway/Adapters/Persistence.Mongodb.Gateway.Tests/ProjectTokensPersistenceAdapterTests.cs new file mode 100644 index 00000000..c64f328f --- /dev/null +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Persistence.Mongodb.Gateway.Tests/ProjectTokensPersistenceAdapterTests.cs @@ -0,0 +1,176 @@ +using MongoDB.Driver; +using ScriptBee.Domain.Model.Project; +using ScriptBee.Domain.Model.User; +using ScriptBee.Persistence.Mongodb.Entity; +using ScriptBee.Persistence.Mongodb.Repository; +using ScriptBee.Tests.Common.Mongodb; + +namespace ScriptBee.Persistence.Mongodb.Tests; + +public class ProjectTokensPersistenceAdapterTests : IClassFixture +{ + private readonly ProjectTokensPersistenceAdapter _adapter; + private readonly IMongoCollection _mongoCollection; + + public ProjectTokensPersistenceAdapterTests(MongoDbFixture fixture) + { + _mongoCollection = fixture.GetCollection("ProjectTokens"); + _adapter = new ProjectTokensPersistenceAdapter( + new MongoRepository(_mongoCollection) + ); + } + + [Fact] + public async Task CreateToken_ShouldPersistToken() + { + var projectId = ProjectId.FromValue("project-create-token"); + var expiresAt = DateTimeOffset.UtcNow.AddDays(7); + + var token = await _adapter.CreateToken( + projectId, + "hashed-token", + "test-token", + new UserRole("editor"), + expiresAt, + TestContext.Current.CancellationToken + ); + + token.ProjectId.ShouldBe(projectId); + token.TokenHash.ShouldBe("hashed-token"); + token.Description.ShouldBe("test-token"); + token.Role.ShouldBe(new UserRole("editor")); + token.ExpiresAt.ShouldBe(expiresAt, TimeSpan.FromSeconds(5)); + token.Id.Value.ShouldNotBeNullOrWhiteSpace(); + + var savedToken = await _mongoCollection + .Find(t => t.ProjectId == projectId.Value) + .FirstOrDefaultAsync(cancellationToken: TestContext.Current.CancellationToken); + + savedToken.ShouldNotBeNull(); + savedToken.ProjectId.ShouldBe(projectId.Value); + savedToken.TokenHash.ShouldBe("hashed-token"); + savedToken.Description.ShouldBe("test-token"); + savedToken.Role.ShouldBe("editor"); + } + + [Fact] + public async Task GetAllForProjectId_ShouldReturnOnlyMatchingProjectTokens() + { + var projectId = ProjectId.FromValue("project-get-all-tokens"); + var otherProjectId = ProjectId.FromValue("other-project"); + + await _mongoCollection.InsertManyAsync( + [ + new MongodbProjectToken + { + ProjectId = projectId.Value, + TokenHash = "hash-one", + Description = "first token", + Role = "owner", + CreatedAt = DateTimeOffset.UtcNow, + ExpiresAt = DateTimeOffset.UtcNow.AddDays(1), + }, + new MongodbProjectToken + { + ProjectId = projectId.Value, + TokenHash = "hash-two", + Description = "second token", + Role = "viewer", + CreatedAt = DateTimeOffset.UtcNow, + ExpiresAt = DateTimeOffset.UtcNow.AddDays(2), + }, + new MongodbProjectToken + { + ProjectId = otherProjectId.Value, + TokenHash = "hash-other", + Description = "other project", + Role = "editor", + CreatedAt = DateTimeOffset.UtcNow, + ExpiresAt = DateTimeOffset.UtcNow.AddDays(3), + }, + ], + cancellationToken: TestContext.Current.CancellationToken + ); + + var tokens = await _adapter.GetAllForProjectId( + projectId, + TestContext.Current.CancellationToken + ); + + tokens.Count.ShouldBe(2); + tokens.ShouldContain(token => + token.ProjectId == projectId + && token.TokenHash == "hash-one" + && token.Description == "first token" + && token.Role == new UserRole("owner") + ); + tokens.ShouldContain(token => + token.ProjectId == projectId + && token.TokenHash == "hash-two" + && token.Description == "second token" + && token.Role == new UserRole("viewer") + ); + } + + [Fact] + public async Task DeleteToken_ShouldRemoveMatchingProjectToken() + { + var projectId = ProjectId.FromValue("project-delete-token"); + var otherProjectId = ProjectId.FromValue("other-project-delete"); + var tokenId = new ProjectTokenId("507f1f77bcf86cd799439011"); + var otherTokenId = new ProjectTokenId("507f1f77bcf86cd799439012"); + var otherProjectTokenId = new ProjectTokenId("507f1f77bcf86cd799439013"); + + await _mongoCollection.InsertManyAsync( + [ + new MongodbProjectToken + { + Id = tokenId.Value, + ProjectId = projectId.Value, + TokenHash = "hash-delete", + Description = "delete me", + Role = "editor", + CreatedAt = DateTimeOffset.UtcNow, + ExpiresAt = DateTimeOffset.UtcNow.AddDays(2), + }, + new MongodbProjectToken + { + Id = otherTokenId.Value, + ProjectId = projectId.Value, + TokenHash = "hash-keep", + Description = "keep me", + Role = "viewer", + CreatedAt = DateTimeOffset.UtcNow, + ExpiresAt = DateTimeOffset.UtcNow.AddDays(3), + }, + new MongodbProjectToken + { + Id = otherProjectTokenId.Value, + ProjectId = otherProjectId.Value, + TokenHash = "hash-other-project", + Description = "other project", + Role = "owner", + CreatedAt = DateTimeOffset.UtcNow, + ExpiresAt = DateTimeOffset.UtcNow.AddDays(4), + }, + ], + cancellationToken: TestContext.Current.CancellationToken + ); + + await _adapter.DeleteToken(projectId, tokenId, TestContext.Current.CancellationToken); + + var remaining = await _mongoCollection + .Find(t => t.ProjectId == projectId.Value) + .ToListAsync(cancellationToken: TestContext.Current.CancellationToken); + + remaining.Count.ShouldBe(1); + remaining[0].Id.ShouldBe(otherTokenId.Value); + remaining[0].TokenHash.ShouldBe("hash-keep"); + + var differentProjectToken = await _mongoCollection + .Find(t => t.ProjectId == otherProjectId.Value) + .FirstOrDefaultAsync(cancellationToken: TestContext.Current.CancellationToken); + differentProjectToken.ShouldNotBeNull(); + differentProjectToken.TokenHash.ShouldBe("hash-other-project"); + } +} diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/GetProjectPermissionsEndpointTests.cs b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/GetProjectPermissionsEndpointTests.cs similarity index 97% rename from ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/GetProjectPermissionsEndpointTests.cs rename to ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/GetProjectPermissionsEndpointTests.cs index ca702f76..fe88c039 100644 --- a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/GetProjectPermissionsEndpointTests.cs +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/GetProjectPermissionsEndpointTests.cs @@ -6,7 +6,7 @@ using ScriptBee.Tests.Common; using ScriptBee.UseCases.Gateway; -namespace ScriptBee.Web.Tests.EndpointDefinitions.Project; +namespace ScriptBee.Web.Tests.EndpointDefinitions.ProjectAccess; public class GetProjectPermissionsEndpointTests(ITestOutputHelper outputHelper) { diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/ManageProjectAccessEndpointTests.cs b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/ManageProjectAccessEndpointTests.cs similarity index 92% rename from ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/ManageProjectAccessEndpointTests.cs rename to ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/ManageProjectAccessEndpointTests.cs index 02eeb5af..3ca0a514 100644 --- a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/ManageProjectAccessEndpointTests.cs +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/ManageProjectAccessEndpointTests.cs @@ -4,9 +4,9 @@ using ScriptBee.Domain.Model.User; using ScriptBee.Tests.Common; using ScriptBee.UseCases.Gateway; -using ScriptBee.Web.EndpointDefinitions.Project.Contracts; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; -namespace ScriptBee.Web.Tests.EndpointDefinitions.Project; +namespace ScriptBee.Web.Tests.EndpointDefinitions.ProjectAccess; public class ManageProjectAccessEndpointTests(ITestOutputHelper outputHelper) { @@ -18,7 +18,7 @@ public async Task GetProjectMembers_ShouldReturnMembers(string responsePath) { var api = new TestApiCaller($"/api/projects/{ProjectId}/members"); var projectId = Domain.Model.Project.ProjectId.FromValue(ProjectId); - var useCase = Substitute.For(); + var useCase = Substitute.For(); useCase .GetProjectMembers(projectId, Arg.Any()) .Returns( @@ -47,7 +47,7 @@ public async Task GetProjectMembers_WhenNoMembers_ShouldReturnEmptyList(string r { var api = new TestApiCaller($"/api/projects/{ProjectId}/members"); var projectId = Domain.Model.Project.ProjectId.FromValue(ProjectId); - var useCase = Substitute.For(); + var useCase = Substitute.For(); useCase .GetProjectMembers(projectId, Arg.Any()) .Returns(Task.FromResult(new List())); @@ -66,7 +66,7 @@ public async Task GetProjectMembers_WhenNoMembers_ShouldReturnEmptyList(string r public async Task UpdateProjectMember_ShouldReturnNoContent() { var api = new TestApiCaller($"/api/projects/{ProjectId}/members/user-a"); - var useCase = Substitute.For(); + var useCase = Substitute.For(); useCase .UpdateProjectMember( Arg.Any(), @@ -114,7 +114,7 @@ public async Task RemoveProjectMember_ShouldReturnNoContent() var api = new TestApiCaller( $"/api/projects/{ProjectId}/members/user-a?memberType=user" ); - var useCase = Substitute.For(); + var useCase = Substitute.For(); useCase .RemoveProjectMember( Arg.Any(), @@ -145,7 +145,7 @@ await useCase public async Task GetAllUsers_ShouldReturnUserList(string responsePath) { var api = new TestApiCaller("/api/users"); - var useCase = Substitute.For(); + var useCase = Substitute.For(); useCase .GetAllUsers(Arg.Any()) .Returns(Task.FromResult(new List { new(new UserId("id-1"), "Alice") })); @@ -165,7 +165,7 @@ public async Task GetAllUsers_ShouldReturnUserList(string responsePath) public async Task GetAvailableRoles_ShouldReturnRoleList(string responsePath) { var api = new TestApiCaller("/api/roles"); - var useCase = Substitute.For(); + var useCase = Substitute.For(); useCase .GetAvailableRoles(Arg.Any()) .Returns( diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/ManageProjectTokensEndpointTests.cs b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/ManageProjectTokensEndpointTests.cs new file mode 100644 index 00000000..5f2fe831 --- /dev/null +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/ManageProjectTokensEndpointTests.cs @@ -0,0 +1,177 @@ +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using ScriptBee.Domain.Model.Project; +using ScriptBee.Domain.Model.User; +using ScriptBee.Tests.Common; +using ScriptBee.UseCases.Gateway; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; + +namespace ScriptBee.Web.Tests.EndpointDefinitions.ProjectAccess; + +public class ManageProjectTokensEndpointTests(ITestOutputHelper outputHelper) +{ + private readonly TestApiCaller _api = new("/api/projects/project-id/tokens"); + + [Theory] + [FilePath("TestData/GetProjectTokens/response.json")] + public async Task GetProjectTokens_ShouldReturnTokens(string responsePath) + { + var projectId = ProjectId.FromValue("project-id"); + var useCase = Substitute.For(); + var createdAt = DateTimeOffset.Parse("2024-02-08T00:00:00+00:00"); + var expiresAt = DateTimeOffset.Parse("2024-03-10T00:00:00+00:00"); + + useCase + .GetProjectTokens(projectId, Arg.Any()) + .Returns( + Task.FromResult( + new List + { + new( + new ProjectTokenId("token-1"), + projectId, + "hash-1", + "CI token", + new UserRole("viewer"), + createdAt, + expiresAt + ), + new( + new ProjectTokenId("token-2"), + projectId, + "hash-2", + "Deploy token", + new UserRole("owner"), + createdAt.AddDays(1), + expiresAt.AddDays(1) + ), + } + ) + ); + + var response = await _api.GetApi( + new TestWebApplicationFactory( + outputHelper, + services => services.AddSingleton(useCase) + ) + ); + + await response.AssertResponse(HttpStatusCode.OK, responsePath); + } + + [Theory] + [FilePath("TestData/GetProjectTokens/empty_response.json")] + public async Task GetProjectTokens_WhenNoTokens_ShouldReturnEmptyList(string responsePath) + { + var projectId = ProjectId.FromValue("project-id"); + var useCase = Substitute.For(); + + useCase + .GetProjectTokens(projectId, Arg.Any()) + .Returns(Task.FromResult(new List())); + + var response = await _api.GetApi( + new TestWebApplicationFactory( + outputHelper, + services => services.AddSingleton(useCase) + ) + ); + + await response.AssertResponse(HttpStatusCode.OK, responsePath); + } + + [Theory] + [FilePath("TestData/CreateProjectToken/response.json")] + public async Task CreateProjectToken_ShouldReturnToken(string responsePath) + { + var useCase = Substitute.For(); + var createdAt = DateTimeOffset.Parse("2024-02-08T00:00:00+00:00"); + var expiresAt = DateTimeOffset.Parse("2024-03-10T00:00:00+00:00"); + var projectId = ProjectId.FromValue("project-id"); + + useCase + .CreateProjectToken(Arg.Any(), Arg.Any()) + .Returns( + Task.FromResult( + new NewProjectTokenResult( + new ProjectToken( + new ProjectTokenId("token-1"), + projectId, + "hash-1", + "CI token", + new UserRole("viewer"), + createdAt, + expiresAt + ), + "plain-token" + ) + ) + ); + + var response = await _api.PostApi( + new TestWebApplicationFactory( + outputHelper, + services => services.AddSingleton(useCase) + ), + new WebCreateProjectTokenRequest("CI token", "viewer", expiresAt) + ); + + await response.AssertResponse(HttpStatusCode.Created, responsePath); + await useCase + .Received(1) + .CreateProjectToken( + new CreateProjectTokenCommand( + projectId, + "CI token", + new UserRole("viewer"), + expiresAt + ), + Arg.Any() + ); + } + + [Fact] + public async Task CreateProjectToken_InvalidBody_ShouldReturnBadRequest() + { + var response = await _api.PostApi( + new TestWebApplicationFactory(outputHelper), + new WebCreateProjectTokenRequest("CI token", "", DateTimeOffset.UtcNow) + ); + + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task RemoveProjectToken_ShouldReturnNoContent() + { + var projectId = ProjectId.FromValue("project-id"); + var useCase = Substitute.For(); + + useCase + .DeleteProjectToken( + projectId, + new ProjectTokenId("token-1"), + Arg.Any() + ) + .Returns(Task.CompletedTask); + + var response = await new TestApiCaller( + "/api/projects/project-id/tokens/token-1" + ).DeleteApi( + new TestWebApplicationFactory( + outputHelper, + services => services.AddSingleton(useCase) + ) + ); + + response.StatusCode.ShouldBe(HttpStatusCode.NoContent); + await useCase + .Received(1) + .DeleteProjectToken( + projectId, + new ProjectTokenId("token-1"), + Arg.Any() + ); + } +} diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/CreateProjectToken/response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/CreateProjectToken/response.json new file mode 100644 index 00000000..17d4b1c3 --- /dev/null +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/CreateProjectToken/response.json @@ -0,0 +1,8 @@ +{ + "id": "token-1", + "token": "plain-token", + "description": "CI token", + "role": "viewer", + "createdAt": "2024-02-08T00:00:00+00:00", + "expiresAt": "2024-03-10T00:00:00+00:00" +} diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetAllUsers/response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetAllUsers/response.json similarity index 100% rename from ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetAllUsers/response.json rename to ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetAllUsers/response.json diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetAvailableRoles/response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetAvailableRoles/response.json similarity index 100% rename from ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetAvailableRoles/response.json rename to ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetAvailableRoles/response.json diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetProjectMembers/empty_response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectMembers/empty_response.json similarity index 100% rename from ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetProjectMembers/empty_response.json rename to ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectMembers/empty_response.json diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetProjectMembers/response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectMembers/response.json similarity index 100% rename from ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetProjectMembers/response.json rename to ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectMembers/response.json diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetProjectPermissions/empty_response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectPermissions/empty_response.json similarity index 100% rename from ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetProjectPermissions/empty_response.json rename to ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectPermissions/empty_response.json diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetProjectPermissions/response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectPermissions/response.json similarity index 100% rename from ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/Project/TestData/GetProjectPermissions/response.json rename to ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectPermissions/response.json diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectTokens/empty_response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectTokens/empty_response.json new file mode 100644 index 00000000..5f72bb88 --- /dev/null +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectTokens/empty_response.json @@ -0,0 +1,3 @@ +{ + "tokens": [] +} diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectTokens/response.json b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectTokens/response.json new file mode 100644 index 00000000..c7a75891 --- /dev/null +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/TestData/GetProjectTokens/response.json @@ -0,0 +1,18 @@ +{ + "tokens": [ + { + "id": "token-1", + "description": "CI token", + "role": "viewer", + "createdAt": "2024-02-08T00:00:00+00:00", + "expiresAt": "2024-03-10T00:00:00+00:00" + }, + { + "id": "token-2", + "description": "Deploy token", + "role": "owner", + "createdAt": "2024-02-09T00:00:00+00:00", + "expiresAt": "2024-03-11T00:00:00+00:00" + } + ] +} diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/Validation/CreateProjectTokenValidatorTests.cs b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/Validation/CreateProjectTokenValidatorTests.cs new file mode 100644 index 00000000..2621b403 --- /dev/null +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/Validation/CreateProjectTokenValidatorTests.cs @@ -0,0 +1,66 @@ +using FluentValidation.TestHelper; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Validation; + +namespace ScriptBee.Web.Tests.EndpointDefinitions.ProjectAccess.Validation; + +public class CreateProjectTokenValidatorTests +{ + private readonly CreateProjectTokenValidator _validator = new(); + + [Fact] + public async Task GivenValidCreateProjectToken_ThenResultHasNoErrors() + { + var request = new WebCreateProjectTokenRequest("CI token", "viewer", DateTimeOffset.UtcNow); + + var result = await _validator.TestValidateAsync( + request, + cancellationToken: TestContext.Current.CancellationToken + ); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public async Task GivenEmptyRole_ThenResultHasErrors() + { + var request = new WebCreateProjectTokenRequest("CI token", "", DateTimeOffset.UtcNow); + + var result = await _validator.TestValidateAsync( + request, + cancellationToken: TestContext.Current.CancellationToken + ); + + result + .ShouldHaveValidationErrorFor(x => x.Role) + .WithErrorMessage("'Role' must not be empty."); + } + + [Fact] + public async Task GivenNullRole_ThenResultHasErrors() + { + var request = new WebCreateProjectTokenRequest("CI token", null!, DateTimeOffset.UtcNow); + + var result = await _validator.TestValidateAsync( + request, + cancellationToken: TestContext.Current.CancellationToken + ); + + result + .ShouldHaveValidationErrorFor(x => x.Role) + .WithErrorMessage("'Role' must not be empty."); + } + + [Fact] + public async Task GivenDefaultExpiresAt_ThenResultHasNoErrors() + { + var request = new WebCreateProjectTokenRequest("CI token", "viewer", default); + + var result = await _validator.TestValidateAsync( + request, + cancellationToken: TestContext.Current.CancellationToken + ); + + result.ShouldNotHaveAnyValidationErrors(); + } +} diff --git a/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/Validation/UpdateProjectMemberCommandValidatorTests.cs b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/Validation/UpdateProjectMemberCommandValidatorTests.cs new file mode 100644 index 00000000..22001df2 --- /dev/null +++ b/ScriptBeeWebApp/test/Gateway/Adapters/Web.Tests/EndpointDefinitions/ProjectAccess/Validation/UpdateProjectMemberCommandValidatorTests.cs @@ -0,0 +1,68 @@ +using FluentValidation.TestHelper; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Contracts; +using ScriptBee.Web.EndpointDefinitions.ProjectAccess.Validation; + +namespace ScriptBee.Web.Tests.EndpointDefinitions.ProjectAccess.Validation; + +public class UpdateProjectMemberCommandValidatorTests +{ + private readonly UpdateProjectMemberCommandValidator _validator = new(); + + [Fact] + public async Task GivenValidUpdateProjectMember_ThenResultHasNoErrors() + { + var command = new WebUpdateProjectMemberCommand("editor", "user"); + + var result = await _validator.TestValidateAsync( + command, + cancellationToken: TestContext.Current.CancellationToken + ); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Fact] + public async Task GivenEmptyRole_ThenResultHasErrors() + { + var command = new WebUpdateProjectMemberCommand("", "user"); + + var result = await _validator.TestValidateAsync( + command, + cancellationToken: TestContext.Current.CancellationToken + ); + + result + .ShouldHaveValidationErrorFor(x => x.Role) + .WithErrorMessage("'Role' must not be empty."); + } + + [Fact] + public async Task GivenEmptyMemberType_ThenResultHasErrors() + { + var command = new WebUpdateProjectMemberCommand("editor", ""); + + var result = await _validator.TestValidateAsync( + command, + cancellationToken: TestContext.Current.CancellationToken + ); + + result + .ShouldHaveValidationErrorFor(x => x.MemberType) + .WithErrorMessage("'Member Type' must not be empty."); + } + + [Fact] + public async Task GivenInvalidMemberType_ThenResultHasErrors() + { + var command = new WebUpdateProjectMemberCommand("editor", "team"); + + var result = await _validator.TestValidateAsync( + command, + cancellationToken: TestContext.Current.CancellationToken + ); + + result + .ShouldHaveValidationErrorFor(x => x.MemberType) + .WithErrorMessage("Member Type must be 'user' or 'group'."); + } +} diff --git a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetAllUsersServiceTests.cs b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetAllUsersServiceTests.cs deleted file mode 100644 index 0c6764c6..00000000 --- a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetAllUsersServiceTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -using NSubstitute; -using ScriptBee.Domain.Model.User; -using ScriptBee.Ports.Permissions; - -namespace ScriptBee.Service.Gateway.Tests; - -public class GetAllUsersServiceTests -{ - private readonly IGetAllUsers _getAllUsers = Substitute.For(); - private readonly GetAllUsersService _service; - - public GetAllUsersServiceTests() - { - _service = new GetAllUsersService(_getAllUsers); - } - - [Fact] - public async Task GetAllUsers_ShouldReturnUsersFromPort() - { - var expectedUsers = new List - { - new(new UserId("id-1"), "Alice"), - new(new UserId("id-2"), "Bob"), - }; - _getAllUsers - .GetAllUsers(Arg.Any()) - .Returns(Task.FromResult(expectedUsers)); - - var result = await _service.GetAllUsers(TestContext.Current.CancellationToken); - - result.ShouldBeEquivalentTo(expectedUsers); - } -} diff --git a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetAvailableRolesServiceTests.cs b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetAvailableRolesServiceTests.cs deleted file mode 100644 index 54f0e2c0..00000000 --- a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetAvailableRolesServiceTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using NSubstitute; -using ScriptBee.Domain.Model.User; -using ScriptBee.Ports.Permissions; - -namespace ScriptBee.Service.Gateway.Tests; - -public class GetAvailableRolesServiceTests -{ - private readonly IGetAvailableRoles _getAvailableRoles = Substitute.For(); - private readonly GetAvailableRolesService _service; - - public GetAvailableRolesServiceTests() - { - _service = new GetAvailableRolesService(_getAvailableRoles); - } - - [Fact] - public async Task GetAvailableRoles_ShouldReturnRolesFromPort() - { - var expectedRoles = new List - { - new("owner", "Full control over the project"), - new("editor", "Can modify project resources"), - new("viewer", "Read-only access to the project"), - }; - _getAvailableRoles - .GetRoles(Arg.Any()) - .Returns(Task.FromResult(expectedRoles)); - - var result = await _service.GetAvailableRoles(TestContext.Current.CancellationToken); - - result.ShouldBeEquivalentTo(expectedRoles); - } -} diff --git a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetProjectMembersServiceTests.cs b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetProjectMembersServiceTests.cs deleted file mode 100644 index 1cbfa421..00000000 --- a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/GetProjectMembersServiceTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using NSubstitute; -using ScriptBee.Domain.Model.Project; -using ScriptBee.Domain.Model.User; -using ScriptBee.Ports.Permissions; - -namespace ScriptBee.Service.Gateway.Tests; - -public class GetProjectMembersServiceTests -{ - private readonly IGetProjectMembers _getProjectMembers = Substitute.For(); - private readonly GetProjectMembersService _service; - - public GetProjectMembersServiceTests() - { - _service = new GetProjectMembersService(_getProjectMembers); - } - - [Fact] - public async Task GetProjectMembers_ShouldReturnMembersFromPort() - { - var projectId = ProjectId.FromValue("project-id"); - var expectedMembers = new List - { - new("user-a", "user", new UserRole("owner")), - new("team-b", "group", new UserRole("viewer")), - }; - _getProjectMembers - .GetProjectMembers(projectId, Arg.Any()) - .Returns(Task.FromResult(expectedMembers)); - - var result = await _service.GetProjectMembers( - projectId, - TestContext.Current.CancellationToken - ); - - result.ShouldBeEquivalentTo(expectedMembers); - } - - [Fact] - public async Task GetProjectMembers_WhenNoMembers_ShouldReturnEmptyList() - { - var projectId = ProjectId.FromValue("empty-project"); - _getProjectMembers - .GetProjectMembers(projectId, Arg.Any()) - .Returns(Task.FromResult(new List())); - - var result = await _service.GetProjectMembers( - projectId, - TestContext.Current.CancellationToken - ); - - result.ShouldBeEmpty(); - } -} diff --git a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/ManageProjectTokensServiceTests.cs b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/ManageProjectTokensServiceTests.cs new file mode 100644 index 00000000..39779398 --- /dev/null +++ b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/ManageProjectTokensServiceTests.cs @@ -0,0 +1,118 @@ +using NSubstitute; +using ScriptBee.Common; +using ScriptBee.Domain.Model.Project; +using ScriptBee.Domain.Model.User; +using ScriptBee.Ports.Permissions; +using ScriptBee.UseCases.Gateway; + +namespace ScriptBee.Service.Gateway.Tests; + +public class ManageProjectTokensServiceTests +{ + private readonly ISecureRandomProvider _secureRandomProvider = + Substitute.For(); + + private readonly IGetAllProjectTokens _getAllProjectTokens = + Substitute.For(); + + private readonly ICreateProjectToken _createProjectToken = + Substitute.For(); + + private readonly IDeleteProjectToken _deleteProjectToken = + Substitute.For(); + + private readonly ManageProjectTokensService _service; + + public ManageProjectTokensServiceTests() + { + _service = new ManageProjectTokensService( + _secureRandomProvider, + _getAllProjectTokens, + _createProjectToken, + _deleteProjectToken + ); + } + + [Fact] + public async Task GetAllProjectTokens() + { + var projectId = ProjectId.FromValue("project-id"); + List projectTokens = + [ + new( + new ProjectTokenId("id"), + projectId, + "hashed-token", + "description", + new UserRole("role"), + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow + ), + ]; + _getAllProjectTokens + .GetAllForProjectId(projectId, Arg.Any()) + .Returns(projectTokens); + + var tokens = await _service.GetProjectTokens( + projectId, + TestContext.Current.CancellationToken + ); + + tokens.ShouldBeEquivalentTo(projectTokens); + } + + [Fact] + public async Task CreateProjectToken() + { + var projectId = ProjectId.FromValue("project-id"); + var userRole = new UserRole("role"); + var expiresAt = DateTimeOffset.UtcNow; + const string tokenHash = "8657B0297E515728040DB40B6E6A8C33F62B744A60159CC3CCF0BF913B458043"; + var command = new CreateProjectTokenCommand(projectId, "description", userRole, expiresAt); + var token = new ProjectToken( + new ProjectTokenId("id"), + projectId, + tokenHash, + "description", + userRole, + DateTimeOffset.UtcNow, + expiresAt + ); + + _secureRandomProvider.GetBytes(32).Returns([.. "ab"u8]); + _createProjectToken + .CreateToken( + projectId, + tokenHash, + "description", + userRole, + expiresAt, + Arg.Any() + ) + .Returns(token); + + var result = await _service.CreateProjectToken( + command, + TestContext.Current.CancellationToken + ); + + result.ShouldBe(new NewProjectTokenResult(token, "sb_at_YWI")); + } + + [Fact] + public async Task DeleteProjectToken() + { + var projectId = ProjectId.FromValue("project-id"); + var tokenId = new ProjectTokenId("id"); + + await _service.DeleteProjectToken( + projectId, + tokenId, + TestContext.Current.CancellationToken + ); + + await _deleteProjectToken + .Received(1) + .DeleteToken(projectId, tokenId, Arg.Any()); + } +} diff --git a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/ManageUsersServiceTests.cs b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/ManageUsersServiceTests.cs index 7a1f8da4..9aca45d8 100644 --- a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/ManageUsersServiceTests.cs +++ b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/ManageUsersServiceTests.cs @@ -1,9 +1,11 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using NSubstitute; +using ScriptBee.Domain.Model.Project; using ScriptBee.Domain.Model.User; using ScriptBee.Ports.Permissions; using ScriptBee.Service.Gateway.Config; +using ScriptBee.UseCases.Gateway; namespace ScriptBee.Service.Gateway.Tests; @@ -13,16 +15,30 @@ public class ManageUsersServiceTests : IDisposable Options.Create(new ScriptBeeUserManagementConfig { UserInfoCacheTimeSeconds = 3 }); private readonly MemoryCache _cache = new(new MemoryCacheOptions()); + + private readonly IGetAvailableRoles _getAvailableRoles = Substitute.For(); + private readonly IGetAllUsers _getAllUsers = Substitute.For(); private readonly IGetOrAddUser _getOrAddUser = Substitute.For(); + private readonly IGetProjectMembers _getProjectMembers = Substitute.For(); + + private readonly IRemoveProjectMember _removeProjectMember = + Substitute.For(); - private readonly ManageUsersService _manageUsersService; + private readonly ISetResourceRole _setResourceRole = Substitute.For(); + + private readonly ManageUsersService _service; public ManageUsersServiceTests() { - _manageUsersService = new ManageUsersService( + _service = new ManageUsersService( _userManagementConfigOptions, _cache, - _getOrAddUser + _getAvailableRoles, + _getOrAddUser, + _getAllUsers, + _getProjectMembers, + _setResourceRole, + _removeProjectMember ); } @@ -37,7 +53,7 @@ public async Task GivenEntryInCache_ThenUserIdIsGetFromCache() var userId = new UserId("user-id"); _cache.Set("external-user-id-existing", userId); - var id = await _manageUsersService.GetUserId( + var id = await _service.GetUserId( "external-user-id-existing", "user-name", TestContext.Current.CancellationToken @@ -57,7 +73,7 @@ public async Task GivenNoEntryInCache_ThenUserIdIsAdded() .GetOrAddUser("external-user-id", "user-name", Arg.Any()) .Returns(userId); - var id = await _manageUsersService.GetUserId( + var id = await _service.GetUserId( "external-user-id", "user-name", TestContext.Current.CancellationToken @@ -68,4 +84,137 @@ await _getOrAddUser .Received(1) .GetOrAddUser("external-user-id", "user-name", Arg.Any()); } + + [Fact] + public async Task GetAvailableRoles_ShouldReturnRolesFromPort() + { + var expectedRoles = new List + { + new("owner", "Full control over the project"), + new("editor", "Can modify project resources"), + new("viewer", "Read-only access to the project"), + }; + _getAvailableRoles + .GetRoles(Arg.Any()) + .Returns(Task.FromResult(expectedRoles)); + + var result = await _service.GetAvailableRoles(TestContext.Current.CancellationToken); + + result.ShouldBeEquivalentTo(expectedRoles); + } + + [Fact] + public async Task GetAllUsers_ShouldReturnUsersFromPort() + { + var expectedUsers = new List + { + new(new UserId("id-1"), "Alice"), + new(new UserId("id-2"), "Bob"), + }; + _getAllUsers + .GetAllUsers(Arg.Any()) + .Returns(Task.FromResult(expectedUsers)); + + var result = await _service.GetAllUsers(TestContext.Current.CancellationToken); + + result.ShouldBeEquivalentTo(expectedUsers); + } + + [Fact] + public async Task GetProjectMembers_ShouldReturnMembersFromPort() + { + var projectId = ProjectId.FromValue("project-id"); + var expectedMembers = new List + { + new("user-a", "user", new UserRole("owner")), + new("team-b", "group", new UserRole("viewer")), + }; + _getProjectMembers + .GetProjectMembers(projectId, Arg.Any()) + .Returns(Task.FromResult(expectedMembers)); + + var result = await _service.GetProjectMembers( + projectId, + TestContext.Current.CancellationToken + ); + + result.ShouldBeEquivalentTo(expectedMembers); + } + + [Fact] + public async Task GetProjectMembers_WhenNoMembers_ShouldReturnEmptyList() + { + var projectId = ProjectId.FromValue("empty-project"); + _getProjectMembers + .GetProjectMembers(projectId, Arg.Any()) + .Returns(Task.FromResult(new List())); + + var result = await _service.GetProjectMembers( + projectId, + TestContext.Current.CancellationToken + ); + + result.ShouldBeEmpty(); + } + + [Fact] + public async Task RemoveProjectMember_ShouldCallPortWithCorrectArguments() + { + var projectId = ProjectId.FromValue("project-id"); + var command = new RemoveProjectMemberCommand(projectId, "user-a", "user"); + + await _service.RemoveProjectMember(command, TestContext.Current.CancellationToken); + + await _removeProjectMember + .Received(1) + .RemoveProjectMember(projectId, "user-a", "user", Arg.Any()); + } + + [Fact] + public async Task UpdateProjectMember_ShouldCallSetRoleForMemberWithCorrectArguments() + { + var projectId = ProjectId.FromValue("project-id"); + var command = new UpdateProjectMemberCommand( + projectId, + "user-a", + "user", + new UserRole("editor") + ); + + await _service.UpdateProjectMember(command, TestContext.Current.CancellationToken); + + await _setResourceRole + .Received(1) + .SetRoleForMember( + "user-a", + "user", + projectId, + new UserRole("editor"), + Arg.Any() + ); + } + + [Fact] + public async Task UpdateProjectMember_ForGroup_ShouldCallSetRoleForMemberWithGroupType() + { + var projectId = ProjectId.FromValue("project-id"); + var command = new UpdateProjectMemberCommand( + projectId, + "dev-team", + "group", + new UserRole("viewer") + ); + + await _service.UpdateProjectMember(command, TestContext.Current.CancellationToken); + + await _setResourceRole + .Received(1) + .SetRoleForMember( + "dev-team", + "group", + projectId, + new UserRole("viewer"), + Arg.Any() + ); + } } diff --git a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/RemoveProjectMemberServiceTests.cs b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/RemoveProjectMemberServiceTests.cs deleted file mode 100644 index adddde14..00000000 --- a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/RemoveProjectMemberServiceTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NSubstitute; -using ScriptBee.Domain.Model.Project; -using ScriptBee.Ports.Permissions; -using ScriptBee.UseCases.Gateway; - -namespace ScriptBee.Service.Gateway.Tests; - -public class RemoveProjectMemberServiceTests -{ - private readonly IRemoveProjectMember _removeProjectMember = - Substitute.For(); - - private readonly RemoveProjectMemberService _service; - - public RemoveProjectMemberServiceTests() - { - _service = new RemoveProjectMemberService(_removeProjectMember); - } - - [Fact] - public async Task RemoveProjectMember_ShouldCallPortWithCorrectArguments() - { - var projectId = ProjectId.FromValue("project-id"); - var command = new RemoveProjectMemberCommand(projectId, "user-a", "user"); - - await _service.RemoveProjectMember(command, TestContext.Current.CancellationToken); - - await _removeProjectMember - .Received(1) - .RemoveProjectMember(projectId, "user-a", "user", Arg.Any()); - } -} diff --git a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/UpdateProjectMemberServiceTests.cs b/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/UpdateProjectMemberServiceTests.cs deleted file mode 100644 index d0a9f9b0..00000000 --- a/ScriptBeeWebApp/test/Gateway/Application/Service.Gateway.Tests/UpdateProjectMemberServiceTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using NSubstitute; -using ScriptBee.Domain.Model.Project; -using ScriptBee.Domain.Model.User; -using ScriptBee.Ports.Permissions; -using ScriptBee.UseCases.Gateway; - -namespace ScriptBee.Service.Gateway.Tests; - -public class UpdateProjectMemberServiceTests -{ - private readonly ISetResourceRole _setResourceRole = Substitute.For(); - private readonly UpdateProjectMemberService _service; - - public UpdateProjectMemberServiceTests() - { - _service = new UpdateProjectMemberService(_setResourceRole); - } - - [Fact] - public async Task UpdateProjectMember_ShouldCallSetRoleForMemberWithCorrectArguments() - { - var projectId = ProjectId.FromValue("project-id"); - var command = new UpdateProjectMemberCommand( - projectId, - "user-a", - "user", - new UserRole("editor") - ); - - await _service.UpdateProjectMember(command, TestContext.Current.CancellationToken); - - await _setResourceRole - .Received(1) - .SetRoleForMember( - "user-a", - "user", - projectId, - new UserRole("editor"), - Arg.Any() - ); - } - - [Fact] - public async Task UpdateProjectMember_ForGroup_ShouldCallSetRoleForMemberWithGroupType() - { - var projectId = ProjectId.FromValue("project-id"); - var command = new UpdateProjectMemberCommand( - projectId, - "dev-team", - "group", - new UserRole("viewer") - ); - - await _service.UpdateProjectMember(command, TestContext.Current.CancellationToken); - - await _setResourceRole - .Received(1) - .SetRoleForMember( - "dev-team", - "group", - projectId, - new UserRole("viewer"), - Arg.Any() - ); - } -} diff --git a/docs/public/gateway_swagger.json b/docs/public/gateway_swagger.json index 675bac4a..e1a7cb32 100644 --- a/docs/public/gateway_swagger.json +++ b/docs/public/gateway_swagger.json @@ -136,11 +136,11 @@ } } }, - "/api/projects/{projectId}/permissions": { - "get": { - "tags": ["Projects", "Permissions"], - "summary": "Get project permissions", - "description": "Get all the permissions for the user associated for the project.", + "/api/projects/{projectId}/scripts": { + "post": { + "tags": ["Scripts"], + "summary": "Create a new script", + "description": "Creates a new script within the specified project.", "parameters": [ { "name": "projectId", @@ -151,25 +151,63 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScriptCommand" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetUserPermissionsForProjectResponse" + "$ref": "#/components/schemas/ScriptData" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/HttpValidationProblemDetails" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" } } } } } - } - }, - "/api/projects/{projectId}/members": { + }, "get": { - "tags": ["Projects", "Access"], - "summary": "Get project members", - "description": "Returns all users and groups with their roles for the project.", + "tags": ["Scripts"], + "summary": "Get all project scripts", + "description": "Retrieves a list of all scripts available in the specified project.", "parameters": [ { "name": "projectId", @@ -186,7 +224,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetProjectMembersResponse" + "$ref": "#/components/schemas/GetScriptDataResponse" } } } @@ -194,11 +232,11 @@ } } }, - "/api/projects/{projectId}/members/{memberId}": { - "put": { - "tags": ["Projects", "Access"], - "summary": "Update project member role", - "description": "Assigns or updates the role for a user or group on the project.", + "/api/projects/{projectId}/files/{fileId}": { + "delete": { + "tags": ["ProjectStructure"], + "summary": "Delete a project structure node", + "description": "Deletes a file or directory (node) from the project structure.", "parameters": [ { "name": "projectId", @@ -209,7 +247,7 @@ } }, { - "name": "memberId", + "name": "fileId", "in": "path", "required": true, "schema": { @@ -217,36 +255,28 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateProjectMemberCommand" - } - } - }, - "required": true - }, "responses": { "204": { "description": "No Content" }, - "400": { - "description": "Bad Request", + "404": { + "description": "Not Found", "content": { - "application/problem+json": { + "application/json": { "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" + "$ref": "#/components/schemas/ProblemDetails" } } } } } - }, - "delete": { - "tags": ["Projects", "Access"], - "summary": "Remove project member", - "description": "Removes a user or group's access from the project.", + } + }, + "/api/projects/{projectId}/scripts/{scriptId}": { + "get": { + "tags": ["Scripts"], + "summary": "Get project script by ID", + "description": "Retrieves metadata about a specific project script.", "parameters": [ { "name": "projectId", @@ -257,72 +287,41 @@ } }, { - "name": "memberId", + "name": "scriptId", "in": "path", "required": true, "schema": { "type": "string" } - }, - { - "name": "memberType", - "in": "query", - "required": true, - "schema": { - "type": "string" - } } ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/users": { - "get": { - "tags": ["Users"], - "summary": "Get all users", - "description": "Returns all registered users for search-ahead.", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetAllUsersResponse" + "$ref": "#/components/schemas/ScriptData" } } } - } - } - } - }, - "/api/roles": { - "get": { - "tags": ["Roles"], - "summary": "Get available roles", - "description": "Returns all roles available for assignment, as defined in the authorization system.", - "responses": { - "200": { - "description": "OK", + }, + "404": { + "description": "Not Found", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetAvailableRolesResponse" + "$ref": "#/components/schemas/ProblemDetails" } } } } } - } - }, - "/api/projects/{projectId}/scripts": { - "post": { + }, + "patch": { "tags": ["Scripts"], - "summary": "Create a new script", - "description": "Creates a new script within the specified project.", + "summary": "Update project script metadata", + "description": "Updates the metadata (name, parameters) of a specific project script.", "parameters": [ { "name": "projectId", @@ -331,21 +330,29 @@ "schema": { "type": "string" } + }, + { + "name": "scriptId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateScriptCommand" + "$ref": "#/components/schemas/UpdateScriptCommand" } } }, "required": true }, "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "content": { "application/json": { "schema": { @@ -373,23 +380,16 @@ } } } - }, - "409": { - "description": "Conflict", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } } } - }, + } + }, + "/api/projects/{projectId}/scripts/{scriptId}/content": { "get": { "tags": ["Scripts"], - "summary": "Get all project scripts", - "description": "Retrieves a list of all scripts available in the specified project.", + "summary": "Get project script content", + "description": "Retrieves the actual code content of a project script.", + "operationId": "GetScriptContent", "parameters": [ { "name": "projectId", @@ -398,27 +398,43 @@ "schema": { "type": "string" } + }, + { + "name": "scriptId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, "200": { "description": "OK", "content": { - "application/json": { + "text/plain": { "schema": { - "$ref": "#/components/schemas/GetScriptDataResponse" + "type": "string" } } } } } - } - }, - "/api/projects/{projectId}/files/{fileId}": { - "delete": { - "tags": ["ProjectStructure"], - "summary": "Delete a project structure node", - "description": "Deletes a file or directory (node) from the project structure.", + }, + "put": { + "tags": ["Scripts"], + "summary": "Update project script content", + "description": "Updates the actual code content of a specific project script.", "parameters": [ { "name": "projectId", @@ -429,7 +445,7 @@ } }, { - "name": "fileId", + "name": "scriptId", "in": "path", "required": true, "schema": { @@ -437,6 +453,16 @@ } } ], + "requestBody": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/Stream" + } + } + }, + "required": true + }, "responses": { "204": { "description": "No Content" @@ -454,11 +480,11 @@ } } }, - "/api/projects/{projectId}/scripts/{scriptId}": { + "/api/projects/{projectId}/files": { "get": { - "tags": ["Scripts"], - "summary": "Get project script by ID", - "description": "Retrieves metadata about a specific project script.", + "tags": ["ProjectStructure"], + "summary": "Get project files", + "description": "Retrieves a paginated list of files and directories within the specified project.", "parameters": [ { "name": "projectId", @@ -469,12 +495,31 @@ } }, { - "name": "scriptId", - "in": "path", - "required": true, + "name": "parentId", + "in": "query", "schema": { "type": "string" } + }, + { + "name": "offset", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": ["integer", "string"], + "format": "int32", + "default": 0 + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": ["integer", "string"], + "format": "int32", + "default": 50 + } } ], "responses": { @@ -483,7 +528,17 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ScriptData" + "$ref": "#/components/schemas/GetProjectFilesResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" } } } @@ -499,11 +554,13 @@ } } } - }, - "patch": { - "tags": ["Scripts"], - "summary": "Update project script metadata", - "description": "Updates the metadata (name, parameters) of a specific project script.", + } + }, + "/api/projects/{projectId}/structure/available-script-types": { + "get": { + "tags": ["ProjectStructure"], + "summary": "Get available script types", + "description": "Retrieves a list of all script languages and types supported by the project.", "parameters": [ { "name": "projectId", @@ -512,53 +569,15 @@ "schema": { "type": "string" } - }, - { - "name": "scriptId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateScriptCommand" - } - } - }, - "required": true - }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ScriptData" - } - } - } - }, - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" + "$ref": "#/components/schemas/GetAvailableScriptTypesResponse" } } } @@ -566,12 +585,11 @@ } } }, - "/api/projects/{projectId}/scripts/{scriptId}/content": { + "/api/projects/{projectId}/permissions": { "get": { - "tags": ["Scripts"], - "summary": "Get project script content", - "description": "Retrieves the actual code content of a project script.", - "operationId": "GetScriptContent", + "tags": ["Projects", "Permissions"], + "summary": "Get project permissions", + "description": "Get all the permissions for the user associated for the project.", "parameters": [ { "name": "projectId", @@ -580,43 +598,56 @@ "schema": { "type": "string" } - }, - { - "name": "scriptId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "responses": { - "404": { - "description": "Not Found", + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "$ref": "#/components/schemas/GetUserPermissionsForProjectResponse" } } } - }, + } + } + } + }, + "/api/projects/{projectId}/members": { + "get": { + "tags": ["Projects", "Access"], + "summary": "Get project members", + "description": "Returns all users and groups with their roles for the project.", + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { "200": { "description": "OK", "content": { - "text/plain": { + "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/GetProjectMembersResponse" } } } } } - }, + } + }, + "/api/projects/{projectId}/members/{memberId}": { "put": { - "tags": ["Scripts"], - "summary": "Update project script content", - "description": "Updates the actual code content of a specific project script.", + "tags": ["Projects", "Access"], + "summary": "Update project member role", + "description": "Assigns or updates the role for a user or group on the project.", "parameters": [ { "name": "projectId", @@ -627,7 +658,7 @@ } }, { - "name": "scriptId", + "name": "memberId", "in": "path", "required": true, "schema": { @@ -637,9 +668,9 @@ ], "requestBody": { "content": { - "text/plain": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Stream" + "$ref": "#/components/schemas/UpdateProjectMemberCommand" } } }, @@ -649,24 +680,22 @@ "204": { "description": "No Content" }, - "404": { - "description": "Not Found", + "400": { + "description": "Bad Request", "content": { - "application/json": { + "application/problem+json": { "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "$ref": "#/components/schemas/HttpValidationProblemDetails" } } } } } - } - }, - "/api/projects/{projectId}/files": { - "get": { - "tags": ["ProjectStructure"], - "summary": "Get project files", - "description": "Retrieves a paginated list of files and directories within the specified project.", + }, + "delete": { + "tags": ["Projects", "Access"], + "summary": "Remove project member", + "description": "Removes a user or group's access from the project.", "parameters": [ { "name": "projectId", @@ -677,72 +706,99 @@ } }, { - "name": "parentId", - "in": "query", + "name": "memberId", + "in": "path", + "required": true, "schema": { "type": "string" } }, { - "name": "offset", - "in": "query", - "schema": { - "pattern": "^-?(?:0|[1-9]\\d*)$", - "type": ["integer", "string"], - "format": "int32", - "default": 0 - } - }, - { - "name": "limit", + "name": "memberType", "in": "query", + "required": true, "schema": { - "pattern": "^-?(?:0|[1-9]\\d*)$", - "type": ["integer", "string"], - "format": "int32", - "default": 50 + "type": "string" } } ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/users": { + "get": { + "tags": ["Users"], + "summary": "Get all users", + "description": "Returns all registered users for search-ahead.", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetProjectFilesResponse" + "$ref": "#/components/schemas/GetAllUsersResponse" } } } - }, - "400": { - "description": "Bad Request", + } + } + } + }, + "/api/roles": { + "get": { + "tags": ["Roles"], + "summary": "Get available roles", + "description": "Returns all roles available for assignment, as defined in the authorization system.", + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "$ref": "#/components/schemas/GetAvailableRolesResponse" } } } - }, - "404": { - "description": "Not Found", + } + } + } + }, + "/api/projects/{projectId}/tokens": { + "get": { + "tags": ["Projects", "Access"], + "summary": "Get project tokens", + "description": "Returns all the tokens associated with the project tokens.", + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "$ref": "#/components/schemas/GetProjectTokensResponse" } } } } } - } - }, - "/api/projects/{projectId}/structure/available-script-types": { - "get": { - "tags": ["ProjectStructure"], - "summary": "Get available script types", - "description": "Retrieves a list of all script languages and types supported by the project.", + }, + "post": { + "tags": ["Projects", "Access"], + "summary": "Create project token", + "description": "Creates a new project token", "parameters": [ { "name": "projectId", @@ -753,13 +809,33 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProjectTokenRequest" + } + } + }, + "required": true + }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetAvailableScriptTypesResponse" + "$ref": "#/components/schemas/CreateProjectTokenResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/HttpValidationProblemDetails" } } } @@ -767,6 +843,36 @@ } } }, + "/api/projects/{projectId}/tokens/{tokenId}": { + "delete": { + "tags": ["Projects", "Access"], + "summary": "Remove project token", + "description": "Removes a token from the project.", + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "tokenId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/api/plugins": { "get": { "tags": ["Plugins"], @@ -2779,6 +2885,49 @@ } } }, + "CreateProjectTokenRequest": { + "required": ["description", "role", "expiresAt"], + "type": "object", + "properties": { + "description": { + "type": ["null", "string"] + }, + "role": { + "type": "string" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "Command used to create a new project toke." + }, + "CreateProjectTokenResponse": { + "required": ["id", "token", "description", "role", "createdAt", "expiresAt"], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "token": { + "type": "string" + }, + "description": { + "type": ["null", "string"] + }, + "role": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + } + } + }, "CreateScriptCommand": { "required": ["path", "language", "parameters"], "type": "object", @@ -3034,6 +3183,18 @@ } } }, + "GetProjectTokensResponse": { + "required": ["tokens"], + "type": "object", + "properties": { + "tokens": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectToken" + } + } + } + }, "GetScriptDataResponse": { "required": ["data"], "type": "object", @@ -3616,6 +3777,29 @@ } } }, + "ProjectToken": { + "required": ["id", "description", "role", "createdAt", "expiresAt"], + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "description": { + "type": ["null", "string"] + }, + "role": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + } + } + }, "RoleInfo": { "required": ["id", "description"], "type": "object", @@ -3752,22 +3936,22 @@ "name": "Projects" }, { - "name": "Permissions" + "name": "Scripts" }, { - "name": "Access" + "name": "ProjectStructure" }, { - "name": "Users" + "name": "Permissions" }, { - "name": "Roles" + "name": "Access" }, { - "name": "Scripts" + "name": "Users" }, { - "name": "ProjectStructure" + "name": "Roles" }, { "name": "Plugins"