Skip to content

Commit 34dd40c

Browse files
authored
feat(ci): adjust company identity creation (#36)
1 parent eb6b7a5 commit 34dd40c

17 files changed

Lines changed: 103 additions & 108 deletions

File tree

charts/dim/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
apiVersion: v2
2121
name: dim
2222
type: application
23-
version: 0.0.2
24-
appVersion: 0.0.2
23+
version: 0.0.3
24+
appVersion: 0.0.3
2525
description: Helm chart for DIM Middle Layer
2626
home: https://github.com/catenax-ng/dim-repo
2727
dependencies:

charts/dim/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ To use the helm chart as a dependency:
2727
dependencies:
2828
- name: dim
2929
repository: https://phil91.github.io/dim-client
30-
version: 0.0.2
30+
version: 0.0.3
3131
```
3232
3333
## Requirements

consortia/argocd-app-templates/appsetup-int.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ spec:
2828
source:
2929
path: charts/dim
3030
repoURL: 'https://github.com/phil91/dim-client.git'
31-
targetRevision: dim-0.0.2
31+
targetRevision: dim-0.0.3
3232
plugin:
3333
env:
3434
- name: AVP_SECRET

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
<Project>
2121
<PropertyGroup>
22-
<VersionPrefix>0.0.2</VersionPrefix>
22+
<VersionPrefix>0.0.3</VersionPrefix>
2323
<VersionSuffix></VersionSuffix>
2424
</PropertyGroup>
2525
</Project>

src/clients/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,25 @@ public record CreateCompanyIdentityRequest(
2626
);
2727

2828
public record Payload(
29-
[property: JsonPropertyName("hostingUrl")] string HostingUrl,
30-
[property: JsonPropertyName("bootstrap")] Bootstrap Bootstrap,
31-
[property: JsonPropertyName("keys")] IEnumerable<Key> Keys
29+
[property: JsonPropertyName("hostingURL")] string HostingUrl,
30+
[property: JsonPropertyName("network")] Network Network,
31+
[property: JsonPropertyName("services")] IEnumerable<Service> Services,
32+
[property: JsonPropertyName("keys")] IEnumerable<Key> Keys,
33+
[property: JsonPropertyName("name")] string Name
3234
);
3335

3436
public record Service(
3537
[property: JsonPropertyName("id")] string Id,
36-
[property: JsonPropertyName("type")] string Type
37-
);
38-
39-
public record Bootstrap(
40-
[property: JsonPropertyName("description")] string Description,
41-
[property: JsonPropertyName("name")] string Name,
42-
[property: JsonPropertyName("protocols")] IEnumerable<string> Protocols
38+
[property: JsonPropertyName("type")] string Type,
39+
[property: JsonPropertyName("serviceEndpoint")] string ServiceEndpoint
4340
);
4441

45-
public record Key(
42+
public record Network(
43+
[property: JsonPropertyName("didMethod")] string DidMethod,
4644
[property: JsonPropertyName("type")] string Type
4745
);
4846

49-
public record Network(
50-
[property: JsonPropertyName("didMethod")] string DidMethod,
47+
public record Key(
5148
[property: JsonPropertyName("type")] string Type
5249
);
5350

@@ -56,3 +53,12 @@ public record CreateCompanyIdentityResponse(
5653
[property: JsonPropertyName("companyId")] Guid CompanyId,
5754
[property: JsonPropertyName("downloadURL")] string DownloadUrl
5855
);
56+
//
57+
// public record UpdateCompanyIdentityRequest(
58+
// [property: JsonPropertyName("didDocUpdates")] DidDocUpdates DidDocUpdates
59+
// );
60+
//
61+
// public record DidDocUpdates(
62+
// [property: JsonPropertyName("removeServices")] IEnumerable<string> RemoveServices,
63+
// [property: JsonPropertyName("addServices")] IEnumerable<Service> AddServices
64+
// );

src/clients/Dim.Clients/Api/Dim/DimClient.cs

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,26 @@
2626

2727
namespace Dim.Clients.Api.Dim;
2828

29-
public class DimClient : IDimClient
29+
public class DimClient(IBasicAuthTokenService basicAuthTokenService, IHttpClientFactory clientFactory)
30+
: IDimClient
3031
{
31-
private readonly IBasicAuthTokenService _basicAuthTokenService;
32-
private readonly IHttpClientFactory _clientFactory;
33-
34-
public DimClient(IBasicAuthTokenService basicAuthTokenService, IHttpClientFactory clientFactory)
35-
{
36-
_basicAuthTokenService = basicAuthTokenService;
37-
_clientFactory = clientFactory;
38-
}
39-
40-
public async Task<CreateCompanyIdentityResponse> CreateCompanyIdentity(BasicAuthSettings dimBasicAuth, string hostingUrl, string baseUrl, string tenantName, bool isIssuer, CancellationToken cancellationToken)
32+
public async Task<CreateCompanyIdentityResponse> CreateCompanyIdentity(BasicAuthSettings dimBasicAuth, Guid tenantId, string hostingUrl, string baseUrl, bool isIssuer, CancellationToken cancellationToken)
4133
{
42-
var client = await _basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimBasicAuth, cancellationToken).ConfigureAwait(false);
34+
var client = await basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimBasicAuth, cancellationToken).ConfigureAwait(false);
4335
var data = new CreateCompanyIdentityRequest(new Payload(
4436
hostingUrl,
45-
new Bootstrap("Holder with IATP", "Holder IATP", Enumerable.Repeat("IATP", 1)),
37+
new Network("web", "production"),
38+
[new Service($"dim:web:{tenantId}", "CredentialService", "https://dis-agent-prod.eu10.dim.cloud.sap/api/v1.0.0/iatp")],
4639
isIssuer ?
47-
Enumerable.Empty<Key>() :
40+
[
41+
new("SIGNING"),
42+
new("SIGNING_VC")
43+
] :
4844
new Key[]
4945
{
50-
new("SIGNING"),
51-
new("SIGNING_VC"),
52-
}));
46+
new("SIGNING")
47+
},
48+
"holder iatp"));
5349
var result = await client.PostAsJsonAsync($"{baseUrl}/api/v2.0.0/companyIdentities", data, JsonSerializerExtensions.Options, cancellationToken)
5450
.CatchingIntoServiceExceptionFor("create-company-identity", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE,
5551
async m =>
@@ -77,15 +73,15 @@ public async Task<CreateCompanyIdentityResponse> CreateCompanyIdentity(BasicAuth
7773

7874
public async Task<JsonDocument> GetDidDocument(string url, CancellationToken cancellationToken)
7975
{
80-
var client = _clientFactory.CreateClient("didDocumentDownload");
76+
var client = clientFactory.CreateClient("didDocumentDownload");
8177
using var result = await client.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
8278
var document = await JsonDocument.ParseAsync(result, cancellationToken: cancellationToken).ConfigureAwait(false);
8379
return document;
8480
}
8581

8682
public async Task<string> CreateApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string tenantName, CancellationToken cancellationToken)
8783
{
88-
var client = await _basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
84+
var client = await basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
8985
var data = new CreateApplicationRequest(new ApplicationPayload(
9086
"catena-x-portal",
9187
$"Catena-X Portal MIW for {tenantName}",
@@ -117,7 +113,7 @@ public async Task<string> CreateApplication(BasicAuthSettings dimAuth, string di
117113

118114
public async Task<string> GetApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationId, CancellationToken cancellationToken)
119115
{
120-
var client = await _basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
116+
var client = await basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
121117
var result = await client.GetAsync($"{dimBaseUrl}/api/v2.0.0/applications/{applicationId}", cancellationToken)
122118
.CatchingIntoServiceExceptionFor("get-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE,
123119
async m =>
@@ -145,7 +141,7 @@ public async Task<string> GetApplication(BasicAuthSettings dimAuth, string dimBa
145141

146142
public async Task AssignApplicationToCompany(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationKey, Guid companyId, CancellationToken cancellationToken)
147143
{
148-
var client = await _basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
144+
var client = await basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
149145
var data = new CompanyIdentityPatch(new ApplicationUpdates(Enumerable.Repeat(applicationKey, 1)));
150146
await client.PatchAsJsonAsync($"{dimBaseUrl}/api/v2.0.0/companyIdentities/{companyId}", data, JsonSerializerExtensions.Options, cancellationToken)
151147
.CatchingIntoServiceExceptionFor("assign-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE,
@@ -158,7 +154,7 @@ await client.PatchAsJsonAsync($"{dimBaseUrl}/api/v2.0.0/companyIdentities/{compa
158154

159155
public async Task<string> GetStatusList(BasicAuthSettings dimAuth, string dimBaseUrl, Guid companyId, CancellationToken cancellationToken)
160156
{
161-
var client = await _basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
157+
var client = await basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
162158
var result = await client.GetAsync($"{dimBaseUrl}/api/v2.0.0/companyIdentities/{companyId}/revocationLists", cancellationToken);
163159
try
164160
{
@@ -185,7 +181,7 @@ public async Task<string> GetStatusList(BasicAuthSettings dimAuth, string dimBas
185181

186182
public async Task<string> CreateStatusList(BasicAuthSettings dimAuth, string dimBaseUrl, Guid companyId, CancellationToken cancellationToken)
187183
{
188-
var client = await _basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
184+
var client = await basicAuthTokenService.GetBasicAuthorizedClient<DimClient>(dimAuth, cancellationToken).ConfigureAwait(false);
189185
var data = new CreateStatusListRequest(new CreateStatusListPaypload(new CreateStatusList("StatusList2021", DateTimeOffset.UtcNow.ToString("yyyyMMdd"), "New revocation list", 2097152)));
190186
var result = await client.PostAsJsonAsync($"{dimBaseUrl}/api/v2.0.0/companyIdentities/{companyId}/revocationLists", data, JsonSerializerExtensions.Options, cancellationToken)
191187
.CatchingIntoServiceExceptionFor("assign-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE,

src/clients/Dim.Clients/Api/Dim/IDimClient.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@ namespace Dim.Clients.Api.Dim;
2424

2525
public interface IDimClient
2626
{
27-
Task<CreateCompanyIdentityResponse> CreateCompanyIdentity(BasicAuthSettings dimBasicAuth, string hostingUrl, string baseUrl, string tenantName, bool isIssuer, CancellationToken cancellationToken);
27+
Task<CreateCompanyIdentityResponse> CreateCompanyIdentity(BasicAuthSettings dimBasicAuth, Guid tenantId, string hostingUrl, string baseUrl, bool isIssuer, CancellationToken cancellationToken);
2828
Task<JsonDocument> GetDidDocument(string url, CancellationToken cancellationToken);
2929
Task<string> CreateApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string tenantName, CancellationToken cancellationToken);
30-
3130
Task<string> GetApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationId, CancellationToken cancellationToken);
3231
Task AssignApplicationToCompany(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationKey, Guid companyId, CancellationToken cancellationToken);
3332
Task<string> GetStatusList(BasicAuthSettings dimAuth, string dimBaseUrl, Guid companyId, CancellationToken cancellationToken);

src/database/Dim.DbAccess/Repositories/ITenantRepository.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ public interface ITenantRepository
4141
Task<(bool Exists, Guid TechnicalUserId, string CompanyName, string Bpn)> GetTenantDataForTechnicalUserProcessId(Guid processId);
4242
Task<(Guid? spaceId, string technicalUserName)> GetSpaceIdAndTechnicalUserName(Guid technicalUserId);
4343
Task<(Guid ExternalId, string? TokenAddress, string? ClientId, byte[]? ClientSecret, byte[]? InitializationVector, int? EncryptionMode)> GetTechnicalUserCallbackData(Guid technicalUserId);
44+
Task<(Guid? DimInstanceId, Guid? CompanyId)> GetDimInstanceIdAndDid(Guid tenantId);
4445
}

src/database/Dim.DbAccess/Repositories/TenantRepository.cs

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,13 @@
2323

2424
namespace Dim.DbAccess.Repositories;
2525

26-
public class TenantRepository : ITenantRepository
26+
public class TenantRepository(DimDbContext context) : ITenantRepository
2727
{
28-
private readonly DimDbContext _context;
29-
30-
public TenantRepository(DimDbContext context)
31-
{
32-
_context = context;
33-
}
34-
3528
public Tenant CreateTenant(string companyName, string bpn, string didDocumentLocation, bool isIssuer, Guid processId, Guid operatorId) =>
36-
_context.Tenants.Add(new Tenant(Guid.NewGuid(), companyName, bpn, didDocumentLocation, isIssuer, processId, operatorId)).Entity;
29+
context.Tenants.Add(new Tenant(Guid.NewGuid(), companyName, bpn, didDocumentLocation, isIssuer, processId, operatorId)).Entity;
3730

3831
public Task<(bool Exists, Guid TenantId, string CompanyName, string Bpn)> GetTenantDataForProcessId(Guid processId) =>
39-
_context.Tenants
32+
context.Tenants
4033
.Where(x => x.ProcessId == processId)
4134
.Select(x => new ValueTuple<bool, Guid, string, string>(true, x.Id, x.CompanyName, x.Bpn))
4235
.SingleOrDefaultAsync();
@@ -45,54 +38,54 @@ public void AttachAndModifyTenant(Guid tenantId, Action<Tenant>? initialize, Act
4538
{
4639
var tenant = new Tenant(tenantId, null!, null!, null!, default, Guid.Empty, Guid.Empty);
4740
initialize?.Invoke(tenant);
48-
_context.Tenants.Attach(tenant);
41+
context.Tenants.Attach(tenant);
4942
modify(tenant);
5043
}
5144

5245
public Task<Guid?> GetSubAccountIdByTenantId(Guid tenantId)
53-
=> _context.Tenants
46+
=> context.Tenants
5447
.Where(x => x.Id == tenantId)
5548
.Select(x => x.SubAccountId)
5649
.SingleOrDefaultAsync();
5750

5851
public Task<(Guid? SubAccountId, string? ServiceInstanceId)> GetSubAccountAndServiceInstanceIdsByTenantId(Guid tenantId)
59-
=> _context.Tenants
52+
=> context.Tenants
6053
.Where(x => x.Id == tenantId)
6154
.Select(x => new ValueTuple<Guid?, string?>(x.SubAccountId, x.ServiceInstanceId))
6255
.SingleOrDefaultAsync();
6356

6457
public Task<(Guid? SubAccountId, string? ServiceBindingName)> GetSubAccountIdAndServiceBindingNameByTenantId(Guid tenantId)
65-
=> _context.Tenants
58+
=> context.Tenants
6659
.Where(x => x.Id == tenantId)
6760
.Select(x => new ValueTuple<Guid?, string?>(x.SubAccountId, x.ServiceBindingName))
6861
.SingleOrDefaultAsync();
6962

7063
public Task<Guid?> GetSpaceId(Guid tenantId)
71-
=> _context.Tenants
64+
=> context.Tenants
7265
.Where(x => x.Id == tenantId)
7366
.Select(x => x.SpaceId)
7467
.SingleOrDefaultAsync();
7568

7669
public Task<Guid?> GetDimInstanceId(Guid tenantId)
77-
=> _context.Tenants
70+
=> context.Tenants
7871
.Where(x => x.Id == tenantId)
7972
.Select(x => x.DimInstanceId)
8073
.SingleOrDefaultAsync();
8174

8275
public Task<(string bpn, string? DownloadUrl, string? Did, Guid? DimInstanceId)> GetCallbackData(Guid tenantId)
83-
=> _context.Tenants
76+
=> context.Tenants
8477
.Where(x => x.Id == tenantId)
8578
.Select(x => new ValueTuple<string, string?, string?, Guid?>(x.Bpn, x.DidDownloadUrl, x.Did, x.DimInstanceId))
8679
.SingleOrDefaultAsync();
8780

8881
public Task<(Guid? DimInstanceId, string HostingUrl, bool IsIssuer)> GetDimInstanceIdAndHostingUrl(Guid tenantId)
89-
=> _context.Tenants
82+
=> context.Tenants
9083
.Where(x => x.Id == tenantId)
9184
.Select(x => new ValueTuple<Guid?, string, bool>(x.DimInstanceId, x.DidDocumentLocation, x.IsIssuer))
9285
.SingleOrDefaultAsync();
9386

9487
public Task<(string? ApplicationId, Guid? CompanyId, Guid? DimInstanceId, bool IsIssuer)> GetApplicationAndCompanyId(Guid tenantId) =>
95-
_context.Tenants
88+
context.Tenants
9689
.Where(x => x.Id == tenantId)
9790
.Select(x => new ValueTuple<string?, Guid?, Guid?, bool>(
9891
x.ApplicationId,
@@ -102,40 +95,40 @@ public void AttachAndModifyTenant(Guid tenantId, Action<Tenant>? initialize, Act
10295
.SingleOrDefaultAsync();
10396

10497
public Task<(bool Exists, Guid? CompanyId, Guid? InstanceId)> GetCompanyAndInstanceIdForBpn(string bpn) =>
105-
_context.Tenants.Where(x => x.Bpn == bpn)
98+
context.Tenants.Where(x => x.Bpn == bpn)
10699
.Select(x => new ValueTuple<bool, Guid?, Guid?>(true, x.CompanyId, x.DimInstanceId))
107100
.SingleOrDefaultAsync();
108101

109102
public void CreateTenantTechnicalUser(Guid tenantId, string technicalUserName, Guid externalId, Guid processId) =>
110-
_context.TechnicalUsers.Add(new TechnicalUser(Guid.NewGuid(), tenantId, externalId, technicalUserName, processId));
103+
context.TechnicalUsers.Add(new TechnicalUser(Guid.NewGuid(), tenantId, externalId, technicalUserName, processId));
111104

112105
public void AttachAndModifyTechnicalUser(Guid technicalUserId, Action<TechnicalUser>? initialize, Action<TechnicalUser> modify)
113106
{
114107
var technicalUser = new TechnicalUser(technicalUserId, Guid.Empty, Guid.Empty, null!, Guid.Empty);
115108
initialize?.Invoke(technicalUser);
116-
_context.TechnicalUsers.Attach(technicalUser);
109+
context.TechnicalUsers.Attach(technicalUser);
117110
modify(technicalUser);
118111
}
119112

120113
public Task<(bool Exists, Guid TenantId)> GetTenantForBpn(string bpn) =>
121-
_context.Tenants.Where(x => x.Bpn == bpn)
114+
context.Tenants.Where(x => x.Bpn == bpn)
122115
.Select(x => new ValueTuple<bool, Guid>(true, x.Id))
123116
.SingleOrDefaultAsync();
124117

125118
public Task<(bool Exists, Guid TechnicalUserId, string CompanyName, string Bpn)> GetTenantDataForTechnicalUserProcessId(Guid processId) =>
126-
_context.TechnicalUsers
119+
context.TechnicalUsers
127120
.Where(x => x.ProcessId == processId)
128121
.Select(x => new ValueTuple<bool, Guid, string, string>(true, x.Id, x.Tenant!.CompanyName, x.Tenant.Bpn))
129122
.SingleOrDefaultAsync();
130123

131124
public Task<(Guid? spaceId, string technicalUserName)> GetSpaceIdAndTechnicalUserName(Guid technicalUserId) =>
132-
_context.TechnicalUsers
125+
context.TechnicalUsers
133126
.Where(x => x.Id == technicalUserId)
134127
.Select(x => new ValueTuple<Guid?, string>(x.Tenant!.SpaceId, x.TechnicalUserName))
135128
.SingleOrDefaultAsync();
136129

137130
public Task<(Guid ExternalId, string? TokenAddress, string? ClientId, byte[]? ClientSecret, byte[]? InitializationVector, int? EncryptionMode)> GetTechnicalUserCallbackData(Guid technicalUserId) =>
138-
_context.TechnicalUsers
131+
context.TechnicalUsers
139132
.Where(x => x.Id == technicalUserId)
140133
.Select(x => new ValueTuple<Guid, string?, string?, byte[]?, byte[]?, int?>(
141134
x.ExternalId,
@@ -145,4 +138,10 @@ public void AttachAndModifyTechnicalUser(Guid technicalUserId, Action<TechnicalU
145138
x.InitializationVector,
146139
x.EncryptionMode))
147140
.SingleOrDefaultAsync();
141+
142+
public Task<(Guid? DimInstanceId, Guid? CompanyId)> GetDimInstanceIdAndDid(Guid tenantId) =>
143+
context.Tenants
144+
.Where(x => x.Id == tenantId)
145+
.Select(x => new ValueTuple<Guid?, Guid?>(x.DimInstanceId, x.CompanyId))
146+
.SingleOrDefaultAsync();
148147
}

src/processes/DimProcess.Executor/DimProcessTypeExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public class DimProcessTypeExecutor(
117117
.ConfigureAwait(false),
118118
ProcessStepTypeId.CREATE_APPLICATION => await dimProcessHandler.CreateApplication(_tenantName, _tenantId, cancellationToken)
119119
.ConfigureAwait(false),
120-
ProcessStepTypeId.CREATE_COMPANY_IDENTITY => await dimProcessHandler.CreateCompanyIdentity(_tenantId, _tenantName, cancellationToken)
120+
ProcessStepTypeId.CREATE_COMPANY_IDENTITY => await dimProcessHandler.CreateCompanyIdentity(_tenantId, cancellationToken)
121121
.ConfigureAwait(false),
122122
ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION => await dimProcessHandler.AssignCompanyApplication(_tenantId, cancellationToken)
123123
.ConfigureAwait(false),

0 commit comments

Comments
 (0)