diff --git a/content/app/guides/multi-app-solution/_index.en.md b/content/app/guides/multi-app-solution/_index.en.md index e69de29bb2d..79f4a07daee 100644 --- a/content/app/guides/multi-app-solution/_index.en.md +++ b/content/app/guides/multi-app-solution/_index.en.md @@ -0,0 +1,65 @@ +--- +title: General approach for making a multi-app solution in Altinn +linktitle: Multi-app solution +description: Considerations and explanations of how to go about when creating a multi-app solution +weight: 250 +aliases: + +- /app/multi-app-solution/ + +--- + +Before continue reading these guidelines, please consider if +a multi-app solution is what you need to fulfill the purpose +of your form(s). + +## What is a multi-app solution? + +A multi-app solution is a solution consisting of two or more +cooperating apps, where typically (as per now) the " +original" application(s) will trigger a creation of a new +instance of the receiving application. As a part of the +instantiation of the receiving application it is possible +prefill the instance with specific data from the running +instance of the original application. + +## Do I need a multi-app solution? + +A multi-app solution is in most cases not a necessary +architectural choice for an Altinn form. + +### Use cases where you can consider utilize a multi-app solution: + +Criteria that should be met if you could consider creating a +multi-app solution: + +- My forms will be answered by users that does not have + Altinn +- It is okay that my receiving forms must be deleted in + order to end the lifecycle of the form. +- The receiving form will act as temporary dashboard in + order to view and/or process the incoming forms, since you + dont have any receiving platform that are processing the + forms. + +### Alternative solution using eFormidling + +It might be that the solution you are looking for is a form, +or multiple forms, that is set up to interact with _ +eFormidling_, which is another service offered by +Digitaliseringsdiriktoratet. Read more about +eFormidling [here](../../development/configuration/eformidling/_index.en.md) +. A solution that is integrated with eFormidling can +implement custom code on process-changes by using the +predefined methods i app-backend. Read more about +that [here](../../development/configuration/process/_index.en.md) +. This custom code can build up a +message, with some form-specific content, that can be sent +to some public institution, instead of using +instantiating a second application being the receiving form. +Be aware that Altinn and eFormidling integration has some +limitations in terms of supported message types, which as +per now is limited to DPO and DPF. +Which means that you will not need to follow this guide. + +{{}} \ No newline at end of file diff --git a/content/app/guides/multi-app-solution/_index.nb.md b/content/app/guides/multi-app-solution/_index.nb.md index e69de29bb2d..739a2db7471 100644 --- a/content/app/guides/multi-app-solution/_index.nb.md +++ b/content/app/guides/multi-app-solution/_index.nb.md @@ -0,0 +1,16 @@ +--- +title: Generell fremgangsmåte for å utvikle en multi-app løsning i Altinn +linktitle: Multi-app løsning +description: Vurderinger som burde gjøres og forklaringer på hvordan å gå frem når man utvikler en multi-app løsning +weight: 250 +aliases: + +- /app/multi-app-solution/ +--- + +Før du leser videre i denne guiden, vær så snill å gjør en vurdering om en multi-app løsning er det du trenger for å realisere skjemaet ditt. + +## Trenger jeg en multi-app løsning? +... + +{{}} \ No newline at end of file diff --git a/content/app/guides/multi-app-solution/considerations/_index.en.md b/content/app/guides/multi-app-solution/considerations/_index.en.md new file mode 100644 index 00000000000..3f62066ed4d --- /dev/null +++ b/content/app/guides/multi-app-solution/considerations/_index.en.md @@ -0,0 +1,11 @@ +--- +title: Considerations you should do before developing a multi-app solution +linktitle: Considerations involving multi-app solution +description: Considerations that should have been made when creating a multi-app solution +weight: 30 +aliases: + +- /app/multi-app-solution/considerations/ + +--- + diff --git a/content/app/guides/multi-app-solution/considerations/_index.nb.md b/content/app/guides/multi-app-solution/considerations/_index.nb.md new file mode 100644 index 00000000000..ac1d5a460d5 --- /dev/null +++ b/content/app/guides/multi-app-solution/considerations/_index.nb.md @@ -0,0 +1,8 @@ +--- +title: Considerations you should do before developing a multi-app solution +linktitle: Considerations involving multi-app solution +description: Considerations that should have been made when creating a multi-app solution +weight: 30 +aliases: +- /app/multi-app-solution/considerations/ +--- \ No newline at end of file diff --git a/content/app/guides/multi-app-solution/instructions/_index.en.md b/content/app/guides/multi-app-solution/instructions/_index.en.md new file mode 100644 index 00000000000..3fc5413724f --- /dev/null +++ b/content/app/guides/multi-app-solution/instructions/_index.en.md @@ -0,0 +1,23 @@ +--- +title: Instructions for making a multi-app solution in Altinn +linktitle: Multi-app solution instructions +description: Explanations of how to go about when creating a general multi-app solution +weight: 20 +aliases: + +- /app/multi-app-solution/instructions/ + +--- + +{{}} + +## General Modifications + +In general, there are a few things that one must remember to +do in the process of developing these applications. + +1. Remember adding custom services to + the `RegisterCustomAppServices` method in `program.cs` +2. If adding any values as prefill for the new instance of + the receiving application, remember to add them to the + data model of the receiving application diff --git a/content/app/guides/multi-app-solution/instructions/_index.nb.md b/content/app/guides/multi-app-solution/instructions/_index.nb.md new file mode 100644 index 00000000000..2a2ac2fc81f --- /dev/null +++ b/content/app/guides/multi-app-solution/instructions/_index.nb.md @@ -0,0 +1,12 @@ +--- +title: Instructions for making a multi-app solution in Altinn +linktitle: Multi-app solution instructions +description: Explanations of how to go about when creating a general multi-app solution +weight: 20 +aliases: + +- /app/multi-app-solution/instructions/ + +--- + +{{}} \ No newline at end of file diff --git a/content/app/guides/multi-app-solution/instructions/receiver-app/_index.en.md b/content/app/guides/multi-app-solution/instructions/receiver-app/_index.en.md new file mode 100644 index 00000000000..2b2847903bd --- /dev/null +++ b/content/app/guides/multi-app-solution/instructions/receiver-app/_index.en.md @@ -0,0 +1,58 @@ +--- +title: Receiver Application +linktitle: Multi-app solution instructions +description: Instructions for setting up the receiver application +weight: 20 +aliases: + +- /app/multi-app-solution/instructions/receiver-app + +--- + +## Getting Data From the Trigger Application + +The receiving application needs much less configuration as a +bare minimum receiver application, at least. The main task +for the receiver application is to fetch the data received +from the trigger application(s) and represent them in a way. +This is done by utilising the `ProcessDataRead` method in +the `DataProcessor` service along with the `UpdateData` +method on the `dataClient`. See example code below: + +```csharp +public async Task ProcessDataRead(Instance instance, Guid? dataId, object data) +{ + bool edited = false; + + if (data.GetType() == typeof(DataModel)) + { + DataModel model = (DataModel)data; + + DataElement attachments = instance.Data.FirstOrDefault(de => de.DataType == "vedlegg"); + + if (attachments != null) + { + _logger.LogInformation("// App 2 // Received data"); + + var instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); + + await _dataClient.UpdateData(model, instanceGuid, typeof(DataModel), instance.Org, instance.AppId, int.Parse(instance.InstanceOwner.PartyId), Guid.Parse(instance.Data.Where(de => de.DataType == "datamodel").First().Id)); + edited = true; + } + } + return await Task.FromResult(edited); +} +``` + +## Stopping a Running Instance + +Since this receiving application, in most cases, will act as +an on-demand dashboard for collecting data from trigger +apps, the application has no natural way of ending its +process, since it is not sent in as any other normal form. +To bypass this obstacle, the incoming forms should either; + +1. be manually deleted after being read, or +2. they must be implemented with a demand of some sort of + user interaction + that will trigger the process to end. \ No newline at end of file diff --git a/content/app/guides/multi-app-solution/instructions/receiver-app/_index.nb.md b/content/app/guides/multi-app-solution/instructions/receiver-app/_index.nb.md new file mode 100644 index 00000000000..1ecbda1eb08 --- /dev/null +++ b/content/app/guides/multi-app-solution/instructions/receiver-app/_index.nb.md @@ -0,0 +1,10 @@ +--- +title: Mottaksapp +linktitle: Multi-app solution instructions +description: Instruksjoner for mottaksappen +weight: 20 +aliases: + +- /app/multi-app-solution/instructions/receiver-app + +--- diff --git a/content/app/guides/multi-app-solution/instructions/trigger-app/_index.en.md b/content/app/guides/multi-app-solution/instructions/trigger-app/_index.en.md new file mode 100644 index 00000000000..3f8167b3cae --- /dev/null +++ b/content/app/guides/multi-app-solution/instructions/trigger-app/_index.en.md @@ -0,0 +1,290 @@ +--- +title: Trigger Application +linktitle: Multi-app solution instructions +description: Instructions for setting up the trigger application +weight: 20 +toc: true +aliases: + +- /app/multi-app-solution/instructions/trigger-app + +--- + +## Setup app to use Maskinporten Integration + +In the process of setting up the application to use the +integration there are three things that needs to be done; + +1. For the application to be able to read the secrets from + Azure keyvault the application need to be configured to + do so. See + the [secrets section](../../../../development/configuration/secrets) + to achieve this. +2. Add the appsettings section example + from [Key Vault Usage](../../preparations/_index.en.md#key-vault-usage) into + the `appsettings.json` file in the application that + should perform the instantiation of the receiving + application. Remember to adapt the section + name `MaskinportenSettings` to the name you chose for the + secrets in Azure keyvault. + +```json +{ + "MaskinportenSettings": { + "Environment": "ver2", + "ClientId": "", + "Scope": "altinn:serviceowner/instances.read", + "EncodedJwk": "", + "ExhangeToAltinnToken": true, + "EnableDebugLog": true + } +} +``` + +3. Modify the `program.cs` file for the same application to + connect to Azure keyvault. Continue reading for a + detailed explanation. + +### Modifying `program.cs` to use Key Vault + +First of all you need to add the MaskinportenHttpClient +service in the function `RegisterCustomAppServices`: + +```csharp +services.AddMaskinportenHttpClient(config.GetSection("MaskinportenSettings")); +``` + +Then you need to add the following +function `ConnectToKeyVault` in the bottom of the file: + +```csharp +static void ConnectToKeyVault(IConfigurationBuilder config) +{ + IConfiguration stageOneConfig = config.Build(); + KeyVaultSettings keyVaultSettings = new KeyVaultSettings(); + stageOneConfig.GetSection("kvSetting").Bind(keyVaultSettings); + if (!string.IsNullOrEmpty(keyVaultSettings.ClientId) && + !string.IsNullOrEmpty(keyVaultSettings.TenantId) && + !string.IsNullOrEmpty(keyVaultSettings.ClientSecret) && + !string.IsNullOrEmpty(keyVaultSettings.SecretUri)) + { + string connectionString = $"RunAs=App;AppId={keyVaultSettings.ClientId};" + + $"TenantId={keyVaultSettings.TenantId};" + + $"AppKey={keyVaultSettings.ClientSecret}"; + AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider(connectionString); + KeyVaultClient keyVaultClient = new KeyVaultClient( + new KeyVaultClient.AuthenticationCallback( + azureServiceTokenProvider.KeyVaultTokenCallback)); + config.AddAzureKeyVault( + keyVaultSettings.SecretUri, keyVaultClient, new DefaultKeyVaultSecretManager()); + } +} +``` + +This function can then be used in the +function `ConfigureWebHostBuilder`. The function already +exist, so just change the content to the following: + +```csharp +void ConfigureWebHostBuilder(IWebHostBuilder builder) +{ + builder.ConfigureAppConfiguration((_, configBuilder) => + { + configBuilder.LoadAppConfig(args); + ConnectToKeyVault(configBuilder); + }); +} +``` + +## Add task to process + +In most cases it is necessary to pass the data the end user +has added to the form, to the receiver application. The data +entered by the end user is represented in the pdf which is +added to the instance object as a part of the `dataTypes` +field with the name `ref-data-as-pdf`. This data element can +be retrieved from the `instance` object in the custom code +that can be added on predefined functions in app-backend. +Read more about how these custom code is +added [here](../../../../development/configuration/process/pre-post-hooks) +. + +However, if you wish the retrieve the pdf data element, your +application must have multiple process tasks. This is due to +the pdf generation is executed after a task has ended. So if +you wish to collect the pdf(s) from the data tasks from the +process, you will need to have at least two tasks, where the +final task is _not_ a data task. The task can be a _confirm_ +or _feedback_ task. + +_NB: It might be that you need to get an updated version of +the instance object from Altinn Storage by calling +the `GetInstance` method on the `IInstanceClient`._ + +### Confirm Task Type + +If using the _confirm_ task type, make sure the +instantiation of the receiver application happens when the +task is finished, i.e. use the `ProcessTaskEnd.End()` +function. This is necessary since the user can go back +to the data task and do changes. + +{{% panel theme="warning" %}} +TODO: Ask Vemund if this require v8 or only recommended. And +if not using v8 keep in mind that accumulated pdfs must be +deleted and action buttons are not implemented so go-to-task +must be used even though not recommended?? +{{% /panel %}} + +### Feedback Task Type + +If using the _feedback_ task type, the instantiation of the +receiving application can be done on the task start +function. Be aware that there has to be some external +triggers that can make sure the application is moved to the +end task event, or else it will stay on the feedback task +forever. The external trigger cannot be the receiving +application since this application will send the request to +end the original request sent from the trigger application +while the trigger application is waiting for the same +request to complete, which will cause a conflict. + +TODO: Is it necessary with multiple steps if pdf of form is +not sent to receiver app? + +### Update `process.bpmn` and `policy.xml` accordingly + +Remember to update the process.bpmn file to match the the +process, and remember adding gateways if you have chosen +the _confirm_ task type. + +Policy.xml also needs updates so read and write operations +can be done on the new task. + +## Trigger the instantiation of the receiving app + +The instantiation of the receiving application is done with +an api call to the running receiving application. The +content of the call will +be the new instance object, which will look something like +this: + +```csharp +var instanceTemplate = new InstansiationInstance +{ + InstanceOwner = new InstanceOwner + { + //OrganisationNumber = [receiverOrgNr], Or + //PersonNumber = [receiverSsnNr], + }, + Prefill = new() + { + {"someDataInReceiverDataModel", someValueFromThisTriggerAppliaction}, + {"moreDataInReceiverDataModel", someStaticValue}, + ... + }, +}; +``` + +The request to create the instance is added to the appClient +which is a custom service that also must be configured. + +Call the method triggering the request in the appClient like +this: + +```csharp +Instance receivingInstance = await _appClient.CreateNewInstance([AppOwnerOrgName], [receivingApp], [instanceTemplate]); +``` + +In the AppClient add this code: + +```csharp +public async Task CreateNewInstance(string org, string app, InstansiationInstance instanceTemplate) +{ + string apiUrl = $"{AppOwnerOrgName}/{receivingApp}/instances/create"; + + string envUrl = $"https://{AppOwnerOrgName}.apps.{_settings.HostName}"; + + _client.BaseAddress = new Uri(envUrl); + + StringContent content = new StringContent(JsonConvert.SerializeObject(instanceTemplate), Encoding.UTF8, "application/json"); + + HttpResponseMessage response = await _client.PostAsync(apiUrl, content); + + if (response.IsSuccessStatusCode) + { + Instance createdInstance = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + + return createdInstance; + } + throw await PlatformHttpException.CreateAsync(response); +} +``` + +## Delivering data to the receiver app + +In order to pass data to the the receiving application there +are several ways of doing this. + +1. Add data as values on the datamodel of the receiving + application by adding the data model field name and the + corresponding value in the `prefill` field of the the + instance template that you created in the _Trigger the + instantiation of the receiving app_ section above. +2. If the intention is to manipulate the texts in Altinn + Inbox for the instances of the receiving application, + use [_presentation + fields_](../../../../development/configuration/messagebox/presentationfields) + . +3. Add data as binary data by doing a POST request to the + Altinn platform on the instance after it is instantiated. + Before some data types can be added, they must be + retrieved from Altinn Platform, such as the pdf for + example, since the application does not have direct + access to this by default. The already + defined `GetBinaryData` method on the dataClient should + be used to get the data and a custom code, called + e.g. `InsertBinaryData`, should be used to insert the + data. + See example code below of both below: + +```csharp +DataElement pdf = updatedInstance.Data.FindLast(d => d.DataType == "ref-data-as-pdf"); +var stream = await _dataClient.GetBinaryData(instance.Org, instance.AppId,int.Parse(instance.InstanceOwner.PartyId), instanceGuid, Guid.Parse(pdf.Id)); + +``` + +```csharp +public async Task InsertBinaryData(string org, string app, string instanceId, string dataType, string contentType, string filename, Stream stream) +{ + string apiUrl = $"{org}/{app}/instances/{instanceId}/data?dataType=vedlegg"; + + DataElement dataElement; + + StreamContent content = new(stream); + content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + if (!string.IsNullOrEmpty(filename)) + { + content.Headers.ContentDisposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment) + { + FileName = filename, + FileNameStar = filename + }; + } + + HttpResponseMessage response = await _client.PostAsync(apiUrl, content); + + if (response.IsSuccessStatusCode) + { + string instancedata = await response.Content.ReadAsStringAsync(); + dataElement = JsonConvert.DeserializeObject(instancedata); + + return dataElement; + } + throw await PlatformHttpException.CreateAsync(response); +} +``` + +4. Data can also be added to the application + using [DataProcessors](../../../../development/logic/dataprocessing) + . \ No newline at end of file diff --git a/content/app/guides/multi-app-solution/instructions/trigger-app/_index.nb.md b/content/app/guides/multi-app-solution/instructions/trigger-app/_index.nb.md new file mode 100644 index 00000000000..a905bedf68f --- /dev/null +++ b/content/app/guides/multi-app-solution/instructions/trigger-app/_index.nb.md @@ -0,0 +1,11 @@ +--- +title: +linktitle: Multi-app solution instructions +description: +weight: 20 +toc: true +aliases: + +- /app/multi-app-solution/instructions/trigger-app + +--- diff --git a/content/app/guides/multi-app-solution/preparations/_index.en.md b/content/app/guides/multi-app-solution/preparations/_index.en.md new file mode 100644 index 00000000000..981867249c2 --- /dev/null +++ b/content/app/guides/multi-app-solution/preparations/_index.en.md @@ -0,0 +1,92 @@ +--- +title: Preparations before making a multi-app solution in Altinn +linktitle: Multi-app solution preparations +description: What preparations that should be done before creating a multi-app solution +weight: 10 +aliases: + +- /app/multi-app-solution/preparations/ + +--- + +A crucial part of the multi-app solution is to make sure the +applications are able, and allowed, to communicate. This is +essential due to the main concept of this solution - the +instantiation of the receiving application. As a part of the +process of the original form you will have logic that +creates a request to the receiving application that starts a +new instance. This request will go to Altinn Storage in +order to create and persist the instance object, but the +request will have credentials from the private user who +logged in to the original form, thus is not allowed to start +a new instance owned by the organisation that owns the +receiving application. As a way to bypass this obstacle, we +can use a Maskinporten integration to authenticate the +request on behalf of the organisation owning the receiving +application. In order to achieve this we need to; + +1. Create the integration + at [Samarbeidsportalen](https://samarbeid.digdir.no/) +2. Store the keys from the integration in Azure Keyvault for + the organisation +3. Set up the application to use Azure Keyvault and the + client to use Maskinporten + +// TODO: ASK RUNE WHAT NEEDED TO BE DONE IN THE AUTH PART + +## Maskinporten Integration + +Before going forward on this step, make sure you have access +to Azure Key Vault for your organization, so the keys +created in the following guide can be added directly into +the secrets in Azure. + +If different people in the +organization have access to different resources needed in +this process, please cooperate and do the following steps on +the same machine. This is recommended to avoid sending +secrets between machines. + +When access to creating secrets in Azure Key vault is +confirmed, please proceed to create the integration; +navigate to +the [Maskinporten Setup guide](../../../../../technology/solutions/cli/configuration/maskinporten-setup) +. + +## Key Vault Usage + +When the integration is created two secrets have to be +placed in Azure Key vault: + +1. The base64 encoded JWT public and private key pair +2. The clientID for the integration + +It is important that the name of these secrets corresponds +to the name of the section in the appsettings file in the +application repository. E.g. if your appsettings section for +the Maskinporten integration section looks like this: + +```json +{ + "MaskinportenSettings": { + "Environment": "ver2", + "ClientId": "", + "Scope": "altinn:serviceowner/instances.read", + "EncodedJwk": "", + "ExhangeToAltinnToken": true, + "EnableDebugLog": true + } +} +``` + +The secrets in Azure keyvault should have names like this: + +``` +MaskinportenSettings--ClientId +MaskinportenSettings--EncodedJwk +``` + +_NB: The secrets is read by the application on start up so +if +changing the secrets after the application is deployed, you +will need to redeploy the application._ diff --git a/content/app/guides/multi-app-solution/preparations/_index.nb.md b/content/app/guides/multi-app-solution/preparations/_index.nb.md new file mode 100644 index 00000000000..2ba11c91fba --- /dev/null +++ b/content/app/guides/multi-app-solution/preparations/_index.nb.md @@ -0,0 +1,10 @@ +--- +title: Preparations before making a multi-app solution in Altinn +linktitle: Multi-app solution preparations +description: What preparations that should be done before creating a multi-app solution +weight: 10 +aliases: + +- /app/multi-app-solution/preparations/ + +---