├── src ├── frontend │ ├── .env │ ├── .dockerignore │ ├── __mocks__ │ │ ├── styleMock.js │ │ └── fileMock.js │ ├── testConfig │ │ ├── setupTests.ts │ │ └── testUtils.ts │ ├── src │ │ ├── language │ │ │ ├── index.ts │ │ │ ├── languages.ts │ │ │ └── texts │ │ │ │ ├── nb.ts │ │ │ │ ├── nn.ts │ │ │ │ └── en.ts │ │ ├── App.css │ │ ├── utils │ │ │ ├── party.ts │ │ │ ├── applicationMetaDataUtils.ts │ │ │ ├── instanceContext.ts │ │ │ ├── instance.ts │ │ │ ├── instanceContext.test.ts │ │ │ ├── instance.test.tsx │ │ │ ├── receiptUrlHelper.ts │ │ │ ├── receipt.ts │ │ │ ├── attachmentsUtils.ts │ │ │ ├── urlHelper.ts │ │ │ ├── receiptUrlHelper.test.ts │ │ │ └── receipt.test.tsx │ │ ├── components │ │ │ ├── AltinnLogo.css │ │ │ ├── molecules │ │ │ │ ├── AltinnSubstatusPaper.test.tsx │ │ │ │ ├── AltinnContentLoader.tsx │ │ │ │ ├── AltinnContentLoader.test.tsx │ │ │ │ ├── AltinnSubstatusPaper.tsx │ │ │ │ ├── AltinnSummaryTable.tsx │ │ │ │ ├── AltinnCollapsibleAttachments.tsx │ │ │ │ └── AltinnModal.tsx │ │ │ ├── atoms │ │ │ │ ├── AltinnContentIcon.tsx │ │ │ │ └── AltinnAttachment.tsx │ │ │ ├── AltinnInformationPaper.tsx │ │ │ ├── index.ts │ │ │ ├── AltinnLogo.tsx │ │ │ ├── AltinnIcon.tsx │ │ │ ├── AltinnLogo.test.tsx │ │ │ └── organisms │ │ │ │ ├── AltinnAppHeader.test.tsx │ │ │ │ ├── AltinnAppHeaderMenu.tsx │ │ │ │ ├── AltinnReceipt.test.tsx │ │ │ │ ├── AltinnAppHeader.tsx │ │ │ │ └── AltinnReceipt.tsx │ │ ├── index.tsx │ │ ├── App.tsx │ │ ├── theme │ │ │ ├── altinnStudioTheme.tsx │ │ │ ├── altinnReceiptTheme.tsx │ │ │ ├── altinnAppTheme.tsx │ │ │ └── commonTheme.tsx │ │ ├── features │ │ │ └── receipt │ │ │ │ ├── Receipt.test.tsx │ │ │ │ └── Receipt.tsx │ │ └── types │ │ │ └── index.ts │ ├── .babelrc │ ├── .yarnrc.yml │ ├── README.md │ ├── prettier.config.js │ ├── webpack.config.production.js │ ├── Dockerfile │ ├── tsconfig.json │ ├── webpack.config.development.js │ ├── webpack.common.js │ ├── .eslintrc.json │ └── package.json └── backend │ └── Altinn.Receipt │ ├── appsettings.Production.json │ ├── Configuration │ ├── KeyVaultSettings.cs │ ├── GeneralSettings.cs │ └── PlatformSettings.cs │ ├── Services │ ├── Interfaces │ │ ├── IProfile.cs │ │ ├── IRegister.cs │ │ └── IStorage.cs │ ├── StorageWrapper.cs │ ├── ProfileWrapper.cs │ └── RegisterWrapper.cs │ ├── Model │ └── ExtendedInstance.cs │ ├── Properties │ └── launchSettings.json │ ├── appsettings.json │ ├── Helpers │ ├── JsonSerializerOptionsProvider.cs │ ├── LanguageHelper.cs │ └── PlatformHttpException.cs │ ├── Clients │ ├── IHttpClientAccessor.cs │ └── HttpClientAccessor.cs │ ├── Health │ └── HealthCheck.cs │ ├── Controllers │ └── ErrorController.cs │ ├── stylecop.json │ ├── Views │ └── Receipt │ │ └── receipt.cshtml │ ├── Altinn.Platform.Receipt.csproj │ ├── Extensions │ └── HttpClientExtension.cs │ └── Telemetry │ └── RequestFilterProcessor.cs ├── .github ├── CODEOWNERS └── workflows │ ├── container-scan.yml │ ├── frontend-test-build.yml │ ├── codeql-analysis.yml │ ├── build-and-analyze-fork.yml │ ├── update-chart.yml │ ├── build-and-analyze.yml │ └── build-and-push-app.yml ├── yarn.lock ├── test ├── selfSignedTestCertificate.pfx ├── Testdata │ ├── Parties.cs │ ├── UserProfiles.cs │ └── Instances.cs ├── GlobalSuppressions.cs ├── appsettings.json ├── Mocks │ ├── JwtCookiePostConfigureOptionsStub.cs │ ├── JwtTokenMock.cs │ └── ConfigurationManagerStub.cs ├── selfSignedTestCertificatePublic.cer ├── Health │ └── HealthCheckTests.cs ├── HttpClientAccessorTest.cs ├── Altinn.Platform.Receipt.Tests.csproj └── LanguageHelperTests.cs ├── .dockerignore ├── renovate.json ├── .deploy └── app.yaml ├── .gitattributes ├── docker-compose.yml ├── Dockerfile ├── stylecop.json ├── Altinn.Receipt.sln ├── .gitignore └── README.md /src/frontend/.env: -------------------------------------------------------------------------------- 1 | PORT=9000 -------------------------------------------------------------------------------- /src/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/frontend/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /src/frontend/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github/CODEOWNERS @altinn/team-altinn-studio 2 | -------------------------------------------------------------------------------- /src/frontend/testConfig/setupTests.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import '@testing-library/jest-dom'; 3 | -------------------------------------------------------------------------------- /src/frontend/src/language/index.ts: -------------------------------------------------------------------------------- 1 | export { languageLookup, getLanguageFromCode } from './languages'; 2 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #1EADF7; 3 | } 4 | html { 5 | font-size: 62.5%!important; 6 | } 7 | -------------------------------------------------------------------------------- /test/selfSignedTestCertificate.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altinn/altinn-receipt/main/test/selfSignedTestCertificate.pfx -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .env 3 | .git 4 | .gitignore 5 | .vs 6 | .vscode 7 | docker-compose.yml 8 | docker-compose.*.yml 9 | */bin 10 | */obj 11 | 12 | */node_modules -------------------------------------------------------------------------------- /src/frontend/src/utils/party.ts: -------------------------------------------------------------------------------- 1 | import { IParty } from '../types'; 2 | 3 | export function renderPartyName(party: IParty) { 4 | if (!party) { 5 | return null; 6 | } 7 | return party.name; 8 | } 9 | -------------------------------------------------------------------------------- /src/frontend/src/components/AltinnLogo.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | height: 24px; 3 | } 4 | 5 | .logo-filter-022F51 { 6 | filter: invert(11%) sepia(34%) saturate(4590%) hue-rotate(188deg) brightness(96%) contrast(98%); 7 | } 8 | -------------------------------------------------------------------------------- /src/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "entry", 7 | "corejs": 2 8 | } 9 | ], 10 | "@babel/preset-react" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import { App } from './App'; 5 | 6 | const container = document.getElementById('root'); 7 | const root = createRoot(container); 8 | root.render(); 9 | -------------------------------------------------------------------------------- /src/frontend/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableMessageNames: false 2 | 3 | enableTelemetry: false 4 | 5 | nodeLinker: node-modules 6 | 7 | plugins: 8 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 9 | spec: "@yarnpkg/plugin-interactive-tools" 10 | 11 | yarnPath: .yarn/releases/yarn-3.8.7.cjs 12 | -------------------------------------------------------------------------------- /src/frontend/src/utils/applicationMetaDataUtils.ts: -------------------------------------------------------------------------------- 1 | import { IApplication, IInstance } from '../types'; 2 | 3 | export const getCurrentTaskData = (appMetaData: IApplication, instance: IInstance) => { 4 | const defaultDatatype = appMetaData.dataTypes.find((element) => element.appLogic !== null); 5 | return instance.data.find((element) => element.dataType === defaultDatatype.id); 6 | }; 7 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | }, 8 | "ApplicationInsights": { 9 | "LogLevel": { 10 | "Default": "Warning", 11 | "System": "Warning", 12 | "Microsoft": "Warning" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Configuration/KeyVaultSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Platform.Receipt.Configuration; 2 | 3 | /// 4 | /// The key vault settings used to fetch values from key vault 5 | /// 6 | public class KeyVaultSettings 7 | { 8 | /// 9 | /// The uri to the key vault 10 | /// 11 | public string SecretUri { get; set; } = string.Empty; 12 | } 13 | -------------------------------------------------------------------------------- /test/Testdata/Parties.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Platform.Register.Models; 2 | 3 | namespace Altinn.Platform.Receipt.Tests.Testdata 4 | { 5 | public static class Parties 6 | { 7 | public static Party Party1 { get; set; } = new Party 8 | { 9 | PartyId = 50001, 10 | SSN = "12345678901", 11 | PartyTypeName = Register.Enums.PartyType.Person 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | ## Running the tests 4 | 5 | ### Lint checks 6 | 7 | 1. Execute `yarn --immutable`. This step is only necessary if you have not already done it, or if you change branches. 8 | 2. Execute `yarn run lint`. 9 | 10 | ### Unit tests 11 | 12 | 1. Execute `yarn --immutable`. This step is only necessary if you have not already done it, or if you change branches. 13 | 2. Execute `yarn run test`. 14 | -------------------------------------------------------------------------------- /src/frontend/src/utils/instanceContext.ts: -------------------------------------------------------------------------------- 1 | import { IInstance, IInstanceContext } from 'src/types'; 2 | 3 | export function buildInstanceContext(instance: IInstance): IInstanceContext { 4 | if (!instance) { 5 | return null; 6 | } 7 | 8 | const instanceContext: IInstanceContext = { 9 | appId: instance.appId, 10 | instanceId: instance.id, 11 | instanceOwnerPartyId: instance.instanceOwner.partyId, 12 | } 13 | 14 | return instanceContext; 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | printWidth: 120, 8 | quoteProps: 'as-needed', 9 | jsxSingleQuote: true, 10 | bracketSpacing: true, 11 | bracketSameLine: false, 12 | arrowParens: 'always', 13 | endOfLine: 'auto', 14 | proseWrap: 'preserve', 15 | htmlWhitespaceSensitivity: 'css', 16 | singleAttributePerLine: true, 17 | }; 18 | -------------------------------------------------------------------------------- /src/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createTheme, MuiThemeProvider } from '@material-ui/core'; 3 | 4 | import AltinnReceiptTheme from 'src/theme/altinnReceiptTheme'; 5 | import Receipt from 'src/features/receipt/Receipt'; 6 | 7 | import './App.css'; 8 | 9 | const theme = createTheme(AltinnReceiptTheme); 10 | 11 | export const App = () => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/frontend/src/language/languages.ts: -------------------------------------------------------------------------------- 1 | import { nb } from './texts/nb'; 2 | import { en } from './texts/en'; 3 | import { nn } from './texts/nn'; 4 | 5 | export const languageLookup: Record>> = { 6 | 'nb': nb, 7 | 'nn': nn, 8 | 'en': en, 9 | }; 10 | 11 | export const getLanguageFromCode = (languageCode: string, defaultLang: string = 'nb'): Record> => { 12 | return languageLookup[languageCode] || languageLookup[defaultLang]; 13 | } 14 | -------------------------------------------------------------------------------- /test/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Test description should be in the name of the test.", Scope = "module")] 9 | -------------------------------------------------------------------------------- /test/Testdata/UserProfiles.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Platform.Profile.Models; 2 | 3 | namespace Altinn.Platform.Receipt.Tests.Testdata 4 | { 5 | public static class UserProfiles 6 | { 7 | public static UserProfile User1 { get; set; } = new UserProfile 8 | { 9 | UserId = 1, 10 | Email = "test@test.no", 11 | PartyId = Parties.Party1.PartyId, 12 | PhoneNumber = "98765432", 13 | UserType = Profile.Enums.UserType.SelfIdentified, 14 | Party = Parties.Party1 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/frontend/webpack.config.production.js: -------------------------------------------------------------------------------- 1 | const TerserPlugin = require('terser-webpack-plugin'); 2 | 3 | const commonConfig = require('./webpack.common'); 4 | 5 | module.exports = { 6 | ...commonConfig, 7 | mode: 'production', 8 | devtool: false, 9 | performance: { 10 | hints: false, 11 | }, 12 | optimization: { 13 | minimize: true, 14 | minimizer: [new TerserPlugin()], 15 | }, 16 | module: { 17 | rules: [ 18 | ...commonConfig.module.rules, 19 | { 20 | test: /\.tsx?/, 21 | use: [{ loader: 'ts-loader' }], 22 | }, 23 | ], 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/frontend/src/components/molecules/AltinnSubstatusPaper.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import AltinnSubstatusPaper from './AltinnSubstatusPaper'; 4 | 5 | describe('AltinnSubstatusPaper', () => { 6 | it('should render label and description', () => { 7 | render( 8 | , 12 | ); 13 | 14 | expect(screen.getByText(/the label/i)).toBeInTheDocument(); 15 | expect(screen.getByText(/the description/i)).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Services/Interfaces/IProfile.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Altinn.Platform.Profile.Models; 4 | 5 | namespace Altinn.Platform.Receipt.Services.Interfaces 6 | { 7 | /// 8 | /// Interface for the Altinn Platform Profile services 9 | /// 10 | public interface IProfile 11 | { 12 | /// 13 | /// Get user profile from userId 14 | /// 15 | /// The user id 16 | /// The user object 17 | public Task GetUser(int userId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Services/Interfaces/IRegister.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Altinn.Platform.Register.Models; 4 | 5 | namespace Altinn.Platform.Receipt.Services.Interfaces 6 | { 7 | /// 8 | /// Interface for the Altinn Platform Register services 9 | /// 10 | public interface IRegister 11 | { 12 | /// 13 | /// Get party object for provided party id 14 | /// 15 | /// The party id 16 | /// The party object 17 | public Task GetParty(int partyId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Model/ExtendedInstance.cs: -------------------------------------------------------------------------------- 1 | using Altinn.Platform.Register.Models; 2 | using Altinn.Platform.Storage.Interface.Models; 3 | 4 | namespace Altinn.Platform.Receipt.Model 5 | { 6 | /// 7 | /// Extended instance object which holds instance metadata and instance owner party object 8 | /// 9 | public class ExtendedInstance 10 | { 11 | /// 12 | /// The instance object 13 | /// 14 | public Instance Instance { get; set; } 15 | 16 | /// 17 | /// The party object related to the instance owner 18 | /// 19 | public Party Party { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Altinn.Platform.Receipt": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development", 8 | "ASPNETCORE_URLS": "http://localhost:5060/" 9 | } 10 | } 11 | }, 12 | "iisSettings": { 13 | "windowsAuthentication": false, 14 | "anonymousAuthentication": true, 15 | "iis": { 16 | "applicationUrl": "http://localhost/Altinn.Platform.Receipt", 17 | "sslPort": 0 18 | }, 19 | "iisExpress": { 20 | "applicationUrl": "http://localhost:5060/", 21 | "sslPort": 0 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image 2 | FROM node:alpine3.16@sha256:f9b54b46639a9017b39eba677cf44c8cb96760ca69dadcc1d4cbd1daea753225 AS generate-receipt-app 3 | 4 | WORKDIR /build 5 | 6 | # Context is ./src, see docker-compose.yaml in src\Altinn.Platform\Altinn.Platform.Receipt\docker-compose.yml 7 | COPY ./frontend/package.json . 8 | COPY ./frontend/yarn.lock . 9 | COPY ./frontend/.yarn/ ./.yarn/ 10 | COPY ./frontend/.yarnrc.yml . 11 | 12 | # Copy shared and receipt code. 13 | COPY ./frontend/shared/ ./shared/ 14 | COPY ./frontend/receipt/ ./receipt/ 15 | 16 | # Install 17 | RUN corepack enable 18 | RUN yarn --immutable 19 | 20 | # Build runtime 21 | RUN yarn workspace receipt-react-app run build; exit 0 22 | CMD ["echo", "done"] -------------------------------------------------------------------------------- /src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["node", "jest"], 4 | "paths": { 5 | "src/*": ["./src/*"], 6 | "testConfig/*": ["./testConfig/*"] 7 | }, 8 | "outDir": "compiled", 9 | "module": "commonjs", 10 | "target": "es6", 11 | "lib": ["es6", "dom", "es7", "esnext"], 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "jsx": "react", 15 | "moduleResolution": "node", 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noImplicitAny": true, 19 | "strictNullChecks": false, 20 | "noUnusedLocals": true, 21 | "esModuleInterop": true 22 | }, 23 | "include": ["**/*.ts", "**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /src/frontend/src/components/molecules/AltinnContentLoader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContentLoader from 'react-content-loader'; 3 | import { AltinnContentIcon } from 'src/components/atoms/AltinnContentIcon'; 4 | 5 | export interface IAltinnContentLoaderProps { 6 | height?: number | string; 7 | width?: number | string; 8 | children?: React.ReactNode; 9 | } 10 | 11 | export const AltinnContentLoader = ({ 12 | width = 400, 13 | height = 200, 14 | children, 15 | }: IAltinnContentLoaderProps) => { 16 | 17 | return ( 18 | 19 | {children ? children : } 20 | 21 | ); 22 | }; 23 | 24 | export default AltinnContentLoader; 25 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>Altinn/renovate-config" 5 | ], 6 | "customManagers": [ 7 | { 8 | "customType": "regex", 9 | "description": "Manage Alpine OS versions in container image tags", 10 | "managerFilePatterns": [ 11 | "/Dockerfile/" 12 | ], 13 | "matchStrings": [ 14 | "(?:FROM\\s+)(?[\\S]+):(?[\\S]+)@(?sha256:[a-f0-9]+)" 15 | ], 16 | "versioningTemplate": "regex:^(?[\\S]*\\d+\\.\\d+(?:\\.\\d+)?(?:[\\S]*)?-alpine-?)(?\\d+)\\.(?\\d+)(?:\\.(?\\d+))?$", 17 | "datasourceTemplate": "docker" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "GeneralSettings": { 3 | "OpenIdWellKnownEndpoint": "http://localhost:5101/authentication/api/v1/openid/", 4 | "Hostname": "at22.altinn.cloud", 5 | "RuntimeCookieName": "AltinnStudioRuntime", 6 | "AttachmentGroupsToHide": "group.formdatahtml;group.formdatasource;group.signaturesource;group.paymentsource;group.activities" 7 | }, 8 | "PlatformSettings": { 9 | "ApiProfileEndpoint": "http://localhost:5101/profile/api/v1/", 10 | "ApiAuthenticationEndpoint": "http://localhost:5101/authentication/api/v1/", 11 | "ApiRegisterEndpoint": "http://localhost:5101/register/api/v1/", 12 | "ApiStorageEndpoint": "http://localhost:5101/storage/api/v1/", 13 | "SubscriptionKey": "Will be inserted during deploy" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "GeneralSettings": { 3 | "OpenIdWellKnownEndpoint": "http://localhost:5040/authentication/api/v1/openid/", 4 | "Hostname": "at22.altinn.cloud", 5 | "RuntimeCookieName": "AltinnStudioRuntime", 6 | "AttachmentGroupsToHide": "group.formdatahtml;group.formdatasource;group.signaturesource;group.paymentsource;group.activities" 7 | }, 8 | "PlatformSettings": { 9 | "ApiProfileEndpoint": "https://platform.tt02.altinn.no/profile/api/v1/", 10 | "ApiAuthenticationEndpoint": "https://platform.tt02.altinn.no/authentication/api/v1/", 11 | "ApiRegisterEndpoint": "https://platform.tt02.altinn.no/register/api/v1/", 12 | "ApiStorageEndpoint": "https://platform.tt02.altinn.no/storage/api/v1/", 13 | "SubscriptionKey": "TestSubscriptionKey" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Services/Interfaces/IStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using Altinn.Platform.Storage.Interface.Models; 5 | 6 | namespace Altinn.Platform.Receipt.Services.Interfaces 7 | { 8 | /// 9 | /// Interface for the Altinn Platform Storage services 10 | /// 11 | public interface IStorage 12 | { 13 | /// 14 | /// Gets an instances based onthe properties of the instanceId 15 | /// 16 | /// The instance owner id 17 | /// Unique id to identify the instance 18 | /// 19 | public Task GetInstance(int instanceOwnerId, Guid instanceGuid); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Configuration/GeneralSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Platform.Receipt.Configuration; 2 | 3 | /// 4 | /// Settings for accessing bridge functionality 5 | /// 6 | public class GeneralSettings 7 | { 8 | /// 9 | /// Open Id Connect Well known endpoint 10 | /// 11 | public string OpenIdWellKnownEndpoint { get; set; } 12 | 13 | /// 14 | /// Hostname 15 | /// 16 | public string Hostname { get; set; } 17 | 18 | /// 19 | /// Name of the cookie for runtime 20 | /// 21 | public string RuntimeCookieName { get; set; } 22 | 23 | /// 24 | /// The attachment groups to hide 25 | /// 26 | public string AttachmentGroupsToHide { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Helpers/JsonSerializerOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Altinn.Platform.Receipt.Helpers; 5 | 6 | /// 7 | /// Provider class for JsonSerializerOptions 8 | /// 9 | public static class JsonSerializerOptionsProvider 10 | { 11 | /// 12 | /// Standard serializer options 13 | /// 14 | public static JsonSerializerOptions Options { get; } = new() 15 | { 16 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 17 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 18 | PropertyNameCaseInsensitive = true, 19 | NumberHandling = JsonNumberHandling.AllowReadingFromString, 20 | 21 | Converters = { new JsonStringEnumConverter() } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /.deploy/app.yaml: -------------------------------------------------------------------------------- 1 | # Only change this file to update the chart 2 | apiVersion: helm.toolkit.fluxcd.io/v2beta1 3 | kind: HelmRelease 4 | metadata: 5 | name: altinn-receipt 6 | namespace: default 7 | spec: 8 | releaseName: altinn-receipt 9 | targetNamespace: default 10 | interval: 5m 11 | install: 12 | remediation: 13 | retries: 1 14 | upgrade: 15 | remediation: 16 | retries: 1 17 | chart: 18 | spec: 19 | version: "0.1.0+b52437b76" # Comes from altinn-studio-ops 20 | chart: altinn-receipt 21 | sourceRef: 22 | kind: HelmRepository 23 | name: altinn-charts 24 | namespace: default 25 | valuesFiles: 26 | - values.yaml 27 | valuesFrom: 28 | - kind: ConfigMap 29 | name: flux-values 30 | valuesKey: values.yaml 31 | values: 32 | image: 33 | tag: "$SHA" 34 | -------------------------------------------------------------------------------- /src/frontend/src/components/atoms/AltinnContentIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function AltinnContentIcon() { 4 | return ( 5 | <> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default AltinnContentIcon; 22 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Clients/IHttpClientAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace Altinn.Platform.Receipt.Clients 4 | { 5 | /// 6 | /// Interface for http client accessor 7 | /// 8 | public interface IHttpClientAccessor 9 | { 10 | /// 11 | /// An Http StorageClient that communicates with the Altinn Platform Storage component 12 | /// 13 | HttpClient StorageClient { get; } 14 | 15 | /// 16 | /// An Http RegisterClient that communicates with the Altinn Platform Register component 17 | /// 18 | HttpClient RegisterClient { get; } 19 | 20 | /// 21 | /// An Http ProfileClient that communicates with the Altinn Platform Profile component. 22 | /// 23 | HttpClient ProfileClient { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/frontend/src/utils/instance.ts: -------------------------------------------------------------------------------- 1 | export function getInstanceOwnerId(): string { 2 | if (!window.location.pathname) { 3 | return ''; 4 | } 5 | return window.location.pathname.split('/')[2]; 6 | } 7 | 8 | export function getInstanceId(): string { 9 | if (!window.location.pathname) { 10 | return ''; 11 | } 12 | return window.location.pathname.split('/')[3]; 13 | } 14 | 15 | export function getArchiveRef(): string { 16 | try { 17 | return getInstanceId().split('-')[4]; 18 | } catch { 19 | return ''; 20 | } 21 | } 22 | 23 | export function getReturnUrl(): string { 24 | if (!globalThis.location.search) { 25 | return ''; 26 | } 27 | 28 | const params = new URLSearchParams(globalThis.location.search) 29 | const lowerCaseParams = new URLSearchParams(); 30 | for (const [name, value] of params) { 31 | lowerCaseParams.append(name.toLowerCase(), value); 32 | } 33 | return lowerCaseParams.get('returnurl'); 34 | } -------------------------------------------------------------------------------- /src/frontend/src/components/molecules/AltinnContentLoader.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render as rtlRender, screen } from '@testing-library/react'; 3 | 4 | import { AltinnContentLoader } from 'src/components/molecules/AltinnContentLoader'; 5 | 6 | const render = (props = {}) => { 7 | const allProps = { 8 | ...props, 9 | }; 10 | 11 | rtlRender(); 12 | }; 13 | 14 | describe('AltinnContentLoader', () => { 15 | it('should show default loader when no children are passed', () => { 16 | render(); 17 | 18 | expect(screen.getByTestId('AltinnContentIcon')).toBeInTheDocument(); 19 | }); 20 | 21 | it('should not show loader when children are passed', () => { 22 | render({ children:
loader
}); 23 | 24 | expect(screen.queryByTestId('AltinnContentIcon')).not.toBeInTheDocument(); 25 | expect(screen.getByTestId('custom-loader')).toBeInTheDocument(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/frontend/src/language/texts/nb.ts: -------------------------------------------------------------------------------- 1 | export const nb: Record> = { 2 | receipt_platform: { 3 | attachments: 'Vedlegg', 4 | date_sent: 'Dato sendt', 5 | date_archived: 'Dato arkivert', 6 | helper_text: 'Det er gjennomført en maskinell kontroll under utfylling, men vi tar forbehold om at det kan bli oppdaget feil under saksbehandlingen og at annen dokumentasjon kan være nødvendig. Vennligst oppgi referansenummer ved eventuelle henvendelser til etaten.', 7 | helper_text_a2lookup: 'Informasjonen som ble hentet ut fra det offentlige, er lagret og signert i Altinn. Klikk på lenke/vedlegg for å se informasjonen i et eget vindu.', 8 | is_sent: 'er sendt inn', 9 | receipt: 'Kvittering', 10 | receiver: 'Mottaker', 11 | reference_number: 'Referansenummer', 12 | sender: 'Avsender', 13 | sent_content: 'Følgende er sendt inn:', 14 | log_out: 'Logg ut', 15 | profile_icon_aria_label: 'Profil ikon knapp', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/frontend/src/language/texts/nn.ts: -------------------------------------------------------------------------------- 1 | export const nn: Record> = { 2 | receipt_platform: { 3 | attachments: 'Vedlegg', 4 | date_sent: 'Dato sendt', 5 | date_archived: 'Dato arkivert', 6 | helper_text: 'Det er gjennomført ein maskinell kontroll under utfylling, men vi tek atterhald om at det kan bli oppdaga feil under sakshandsaminga og at annan dokumentasjon kan vere naudsynt. Ver venleg oppgi referansenummer ved eventuelle førespurnadar til etaten.', 7 | helper_text_a2lookup: 'Informasjonen som blei henta ut frå det offentlege, er lagra og signert i Altinn. Klikk på lenke/vedlegg for å sjå informasjonen i eit eige vindu.', 8 | is_sent: 'er sendt inn', 9 | receipt: 'Kvittering', 10 | receiver: 'Mottakar', 11 | reference_number: 'Referansenummer', 12 | sender: 'Avsendar', 13 | sent_content: 'Følgjande er sendt inn:', 14 | log_out: 'Logg ut', 15 | profile_icon_aria_label: 'Profil ikon knapp', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/frontend/src/components/AltinnInformationPaper.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme, Paper } from '@material-ui/core'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import React from 'react'; 4 | import altinnTheme from '../theme/altinnStudioTheme'; 5 | 6 | const theme = createTheme(altinnTheme); 7 | const useStyles = makeStyles({ 8 | paper: { 9 | background: theme.altinnPalette.primary.white, 10 | boxShadow: '1px 1px 4px rgba(0, 0, 0, 0.25)', 11 | borderRadius: 0, 12 | fontSize: 16, 13 | padding: 24, 14 | fontFamily: 'Altinn-DIN', 15 | }, 16 | }); 17 | 18 | export interface IAltinnInformationPaperProps { 19 | children: JSX.Element[] | JSX.Element; 20 | } 21 | 22 | export default function AltinnInformationPaper({ 23 | children, 24 | } : IAltinnInformationPaperProps) { 25 | const classes = useStyles(); 26 | return ( 27 | 31 | {children} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/frontend/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AltinnIcon } from 'src/components/AltinnIcon'; 2 | export { default as AltinnCollapsibleAttachments } from 'src/components/molecules/AltinnCollapsibleAttachments'; 3 | export { default as AltinnContentLoader } from 'src/components/molecules/AltinnContentLoader'; 4 | export { default as AltinnContentIcon } from 'src/components/atoms/AltinnContentIcon'; 5 | export { default as AltinnInformationPaper } from 'src/components/AltinnInformationPaper'; 6 | export { default as AltinnSubstatusPaper } from 'src/components/molecules/AltinnSubstatusPaper'; 7 | export { default as AltinnAttachment } from 'src/components/atoms/AltinnAttachment'; 8 | export { default as AltinnReceipt } from 'src/components/organisms/AltinnReceipt'; 9 | export { default as AltinnModal } from 'src/components/molecules/AltinnModal'; 10 | export { default as AltinnAppHeader } from 'src/components/organisms/AltinnAppHeader'; 11 | export { default as AltinnLogo } from 'src/components/AltinnLogo'; 12 | -------------------------------------------------------------------------------- /src/frontend/src/language/texts/en.ts: -------------------------------------------------------------------------------- 1 | export const en: Record> = { 2 | receipt_platform: { 3 | attachments: 'Attachments', 4 | date_sent: 'Date sent', 5 | date_archived: 'Date archived', 6 | helper_text: 'A mechanical check has been completed while filling in, but we reserve the right to detect errors during the processing of the case and that other documentation may be necessary. Please provide the reference number in case of any inquiries to the agency.', 7 | helper_text_a2lookup: 'The information that was collected from the public sector is saved and signed in Altinn. Click the link/attachment to view the information in a separate window.', 8 | is_sent: 'is submitted', 9 | receipt: 'Receipt', 10 | receiver: 'Receiver', 11 | reference_number: 'Reference number', 12 | sender: 'Sender', 13 | sent_content: 'The following is submitted:', 14 | log_out: 'Log out', 15 | profile_icon_aria_label: 'Profile icon button', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Health/HealthCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | 5 | namespace Altinn.Platform.Receipt.Health 6 | { 7 | /// 8 | /// Health check service configured in startup https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks 9 | /// Listen to 10 | /// 11 | public class HealthCheck : IHealthCheck 12 | { 13 | /// 14 | /// Verifies the health status 15 | /// 16 | /// The healtcheck context 17 | /// The cancellationtoken 18 | /// The health check result 19 | public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) 20 | { 21 | return Task.FromResult( 22 | HealthCheckResult.Healthy("A healthy result.")); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/frontend/src/components/molecules/AltinnSubstatusPaper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, Typography } from '@material-ui/core'; 3 | import AltinnInformationPaper from '../AltinnInformationPaper'; 4 | 5 | export interface IInformationPaperProps { 6 | label: string; 7 | description: string; 8 | } 9 | 10 | export default function AltinnSubstatusPaper({ 11 | label, 12 | description, 13 | }: IInformationPaperProps) { 14 | return ( 15 | 16 | 20 | 21 | 22 | {label} 23 | 24 | 25 | 26 | 27 | {description} 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /test/Testdata/Instances.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Altinn.Platform.Storage.Interface.Models; 4 | 5 | namespace Altinn.Platform.Receipt.Tests.Testdata 6 | { 7 | public static class Instances 8 | { 9 | public static Instance Instance1 { get; set; } = new Instance 10 | { 11 | Id = "1000/1c3a4b9d-cbbe-4146-b370-4164e925812b", 12 | InstanceOwner = new InstanceOwner 13 | { 14 | PartyId = Parties.Party1.PartyId.ToString() 15 | }, 16 | AppId = "tdd/auth-level-3", 17 | Org = "tdd", 18 | Created = DateTime.Parse("2019-07-31T09:57:23.4729995Z"), 19 | LastChanged = DateTime.Parse("2019-07-31T09:57:23.4729995Z"), 20 | Process = new ProcessState 21 | { 22 | CurrentTask = new ProcessElementInfo 23 | { 24 | ElementId = "FormFilling" 25 | }, 26 | Started = DateTime.Parse("2019-07-31T09:57:23.4729995Z") 27 | } 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/frontend/src/theme/altinnStudioTheme.tsx: -------------------------------------------------------------------------------- 1 | import { commonTheme } from './commonTheme'; 2 | 3 | const theme = { 4 | ...commonTheme, 5 | overrides: { 6 | MuiToolbar: { 7 | regular: { 8 | '@media (min-width: 600px)': { 9 | minHeight: 55, 10 | }, 11 | }, 12 | }, 13 | MuiTypography: { 14 | h1: { 15 | fontSize: 36, 16 | }, 17 | h2: { 18 | fontSize: 20, 19 | fontWeight: 500, 20 | }, 21 | body1: { 22 | fontSize: 16, 23 | }, 24 | caption: { 25 | fontSize: 14, 26 | }, 27 | }, 28 | }, 29 | props: { 30 | MuiButtonBase: { 31 | disableRipple: true, 32 | disableTouchRipple: true, 33 | }, 34 | }, 35 | sharedStyles: { 36 | boxShadow: '0px 0px 4px rgba(0, 0, 0, 0.25)', 37 | linkBorderBottom: '1px solid #0062BA', 38 | mainPaddingLeft: 73, 39 | leftDrawerMenuClosedWidth: 65, 40 | }, 41 | typography: { 42 | htmlFontSize: 16, 43 | useNextVariants: true, 44 | }, 45 | }; 46 | 47 | export default theme; 48 | -------------------------------------------------------------------------------- /src/frontend/src/components/AltinnLogo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import appTheme from '../theme/altinnAppTheme'; 3 | import './AltinnLogo.css'; 4 | 5 | export interface IAltinnLogoProps { 6 | color: string; 7 | } 8 | 9 | function getLogoColor(color: string) { 10 | const colors = appTheme.altinnPalette.primary; 11 | switch (color) { 12 | case 'white': 13 | case colors.white: 14 | return 'white'; 15 | 16 | case 'blueDark': 17 | case colors.blueDark: 18 | return 'blue'; 19 | 20 | default: 21 | return 'black'; 22 | } 23 | } 24 | 25 | export const AltinnLogo = ({ color }: IAltinnLogoProps) => { 26 | const logoColor = getLogoColor(color); 27 | let filterClass = ''; 28 | 29 | if (logoColor === 'black') { 30 | filterClass = ` logo-filter-${color.replace('#', '')}`; 31 | } 32 | 33 | return ( 34 | 40 | ); 41 | }; 42 | 43 | export default AltinnLogo; 44 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Controllers/ErrorController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Altinn.Platform.Receipt.Controllers 5 | { 6 | /// 7 | /// Handles the presentation of unhandled exceptions during the execution of a request. 8 | /// 9 | [ApiController] 10 | [ApiExplorerSettings(IgnoreApi = true)] 11 | [AllowAnonymous] 12 | [Route("receipt/api/v1")] 13 | public class ErrorController : ControllerBase 14 | { 15 | /// 16 | /// Create a response with a new instance with limited information. 17 | /// 18 | /// 19 | /// This method cannot be called directly. It is used by the API framework as a way to output ProblemDetails 20 | /// if there has been an unhandled exception. 21 | /// 22 | /// A new instance. 23 | [Route("error")] 24 | public IActionResult Error() => Problem(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/frontend/webpack.config.development.js: -------------------------------------------------------------------------------- 1 | const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin'); 2 | 3 | const commonConfig = require('./webpack.common'); 4 | 5 | module.exports = { 6 | ...commonConfig, 7 | mode: 'development', 8 | devtool: 'eval', 9 | performance: { 10 | hints: 'warning', 11 | }, 12 | module: { 13 | rules: [ 14 | ...commonConfig.module.rules, 15 | 16 | { 17 | test: /\.tsx?/, 18 | use: [ 19 | { 20 | loader: 'ts-loader', 21 | options: { transpileOnly: true }, 22 | }, 23 | ], 24 | }, 25 | { 26 | enforce: 'pre', 27 | test: /\.js$/, 28 | loader: 'source-map-loader', 29 | }, 30 | ], 31 | }, 32 | plugins: [...commonConfig.plugins, new ForkTsCheckerNotifierWebpackPlugin()], 33 | devServer: { 34 | historyApiFallback: true, 35 | allowedHosts: 'all', 36 | headers: { 'Access-Control-Allow-Origin': '*' }, 37 | client: { 38 | overlay: { 39 | errors: true, 40 | warnings: false, 41 | }, 42 | }, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/frontend/src/theme/altinnReceiptTheme.tsx: -------------------------------------------------------------------------------- 1 | import { commonTheme } from './commonTheme'; 2 | 3 | const AltinnReceiptTheme = { 4 | ...commonTheme, 5 | overrides: { 6 | MuiToolbar: { 7 | regular: { 8 | '@media (min-width: 600px)': { 9 | minHeight: 55, 10 | }, 11 | }, 12 | }, 13 | MuiTypography: { 14 | h1: { 15 | fontSize: 36, 16 | }, 17 | h2: { 18 | fontSize: 28, 19 | }, 20 | h3: { 21 | fontSize: 20, 22 | }, 23 | body1: { 24 | fontSize: 16, 25 | }, 26 | body2: { 27 | fontSize: 14, 28 | }, 29 | caption: { 30 | fontSize: 14, 31 | }, 32 | }, 33 | }, 34 | props: { 35 | MuiButtonBase: { 36 | disableRipple: true, 37 | disableTouchRipple: true, 38 | }, 39 | }, 40 | sharedStyles: { 41 | boxShadow: '0px 0px 4px rgba(0, 0, 0, 0.25)', 42 | linkBorderBottom: '1px solid #0062BA', 43 | mainPaddingLeft: 73, 44 | }, 45 | typography: { 46 | htmlFontSize: 16, 47 | useNextVariants: true, 48 | }, 49 | }; 50 | 51 | export default AltinnReceiptTheme; 52 | -------------------------------------------------------------------------------- /src/frontend/src/utils/instanceContext.test.ts: -------------------------------------------------------------------------------- 1 | import type { IInstanceContext, IInstance } from '../types'; 2 | import { buildInstanceContext } from './instanceContext'; 3 | 4 | describe('instanceContext', () => { 5 | const partyId = '1337'; 6 | const appId = 'tdd/enapp'; 7 | const instaceId = `${partyId}/super-secret-uuid-000`; 8 | const mockInstance: IInstance = { 9 | id: instaceId, 10 | appId: appId, 11 | instanceOwner: { 12 | partyId: partyId, 13 | }, 14 | } as IInstance; 15 | 16 | it('should build a valid instance context', () => { 17 | const expected: IInstanceContext = { 18 | appId: appId, 19 | instanceId: instaceId, 20 | instanceOwnerPartyId: partyId, 21 | }; 22 | const actual = buildInstanceContext(mockInstance); 23 | 24 | expect(actual).toEqual(expected); 25 | }); 26 | 27 | it('should handle null input gracefully', () => { 28 | const actual = buildInstanceContext(null); 29 | 30 | expect(actual).toBeNull(); 31 | }); 32 | 33 | it('should handle undefined input gracefully', () => { 34 | const actual = buildInstanceContext(undefined); 35 | 36 | expect(actual).toBeNull(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # General setting that applies Git's binary detection for file-types not specified below 2 | # Meaning, for 'text-guessed' files: 3 | # use normalization (convert crlf -> lf on commit, i.e. use `text` setting) 4 | # & do unspecified diff behavior (if file content is recognized as text & filesize < core.bigFileThreshold, do text diff on file changes) 5 | * text=auto 6 | 7 | # Override with explicit specific settings for known and/or likely text files in our repo that should be normalized 8 | # where diff{=optional_pattern} means "do text diff {with specific text pattern} and -diff means "don't do text diffs". 9 | # Unspecified diff behavior is decribed above 10 | *.cer text -diff 11 | *.cmd text 12 | *.cs text diff=csharp 13 | *.csproj text 14 | *.css text diff=css 15 | Dockerfile text 16 | *.json text 17 | *.md text diff=markdown 18 | *.msbuild text 19 | *.pem text -diff 20 | *.ps1 text 21 | *.sln text 22 | *.yaml text 23 | *.yml text 24 | 25 | # Files that should be treated as binary ('binary' is a macro for '-text -diff', i.e. "don't normalize or do text diff on content") 26 | *.jpeg binary 27 | *.pfx binary 28 | *.png binary -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | networks: 4 | altinnplatform_network: 5 | external: false 6 | 7 | services: 8 | altinn_platform_receipt: 9 | container_name: altinn-platform-receipt 10 | image: altinn-platform-receipt:latest 11 | restart: always 12 | networks: 13 | - altinnplatform_network 14 | environment: 15 | - ASPNETCORE_ENVIRONMENT=Development 16 | - ASPNETCORE_URLS=http://+:5060 17 | - PlatformSettings:ApiProfileEndpoint=http://host.docker.internal:5101/profile/api/v1/ 18 | - PlatformSettings:ApiAuthenticationEndpoint=http://host.docker.internal:5101//authentication/api/v1/ 19 | - PlatformSettings:ApiRegisterEndpoint=http://host.docker.internal:5101/register/api/v1/ 20 | - PlatformSettings:ApiStorageEndpoint=http://host.docker.internal:5101/storage/api/v1/ 21 | - GeneralSettings:OpenIdWellKnownEndpoint=http://host.docker.internal:5101/authentication/api/v1/openid/ 22 | - GeneralSettings:Hostname=at22.altinn.cloud 23 | - GeneralSettings:RuntimeCookieName=AltinnStudioRuntime 24 | ports: 25 | - "5060:5060" 26 | extra_hosts: 27 | - host.docker.internal:host-gateway 28 | build: 29 | context: . 30 | dockerfile: Dockerfile 31 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Helpers/LanguageHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Platform.Receipt.Helpers; 2 | 3 | /// 4 | /// Provides helper methods for language-related operations. 5 | /// 6 | public static class LanguageHelper 7 | { 8 | /// 9 | /// Gets the language from the Altinn persistence cookie. 10 | /// 11 | /// The value of the Altinn persistence cookie containing language information. 12 | /// The language code ('en', 'nb', 'nn') extracted from the Altinn persistence cookie, or empty string if language not found. 13 | public static string GetLanguageFromAltinnPersistenceCookie(string cookieValue) 14 | { 15 | if (string.IsNullOrEmpty(cookieValue)) 16 | { 17 | return string.Empty; 18 | } 19 | 20 | if (cookieValue.Contains("UL=1033")) 21 | { 22 | return "en"; 23 | } 24 | 25 | if (cookieValue.Contains("UL=1044")) 26 | { 27 | return "nb"; 28 | } 29 | 30 | if (cookieValue.Contains("UL=2068")) 31 | { 32 | return "nn"; 33 | } 34 | 35 | return string.Empty; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Mocks/JwtCookiePostConfigureOptionsStub.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using AltinnCore.Authentication.JwtCookie; 4 | 5 | using Microsoft.AspNetCore.Authentication.Cookies; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Altinn.Platform.Receipt.Tests.Mocks 9 | { 10 | public class JwtCookiePostConfigureOptionsStub : IPostConfigureOptions 11 | { 12 | /// 13 | public void PostConfigure(string name, JwtCookieOptions options) 14 | { 15 | if (string.IsNullOrEmpty(options.JwtCookieName)) 16 | { 17 | options.JwtCookieName = JwtCookieDefaults.CookiePrefix + name; 18 | } 19 | 20 | if (options.CookieManager == null) 21 | { 22 | options.CookieManager = new ChunkingCookieManager(); 23 | } 24 | 25 | if (!string.IsNullOrEmpty(options.MetadataAddress)) 26 | { 27 | if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) 28 | { 29 | options.MetadataAddress += "/"; 30 | } 31 | } 32 | 33 | options.MetadataAddress += ".well-known/openid-configuration"; 34 | options.ConfigurationManager = new ConfigurationManagerStub(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/container-scan.yml: -------------------------------------------------------------------------------- 1 | name: Receipt Scan 2 | 3 | on: 4 | schedule: 5 | - cron: '0 8 * * 1,4' 6 | push: 7 | branches: [main] 8 | paths: 9 | - 'src/**' 10 | - 'Dockerfile' 11 | pull_request: 12 | branches: [main] 13 | types: [opened, synchronize, reopened] 14 | paths: 15 | - 'src/**' 16 | - 'Dockerfile' 17 | jobs: 18 | scan: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | steps: 23 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 24 | - name: Build the Docker image 25 | run: docker build . --tag altinn-receipt:${{github.sha}} 26 | 27 | - name: Run Trivy vulnerability scanner 28 | uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 29 | with: 30 | image-ref: 'altinn-receipt:${{ github.sha }}' 31 | format: 'table' 32 | exit-code: '1' 33 | ignore-unfixed: true 34 | vuln-type: 'os,library' 35 | severity: 'CRITICAL,HIGH' 36 | env: 37 | TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db,aquasec/trivy-db,ghcr.io/aquasecurity/trivy-db 38 | TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db,aquasec/trivy-java-db,ghcr.io/aquasecurity/trivy-java-db 39 | -------------------------------------------------------------------------------- /src/frontend/webpack.common.js: -------------------------------------------------------------------------------- 1 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | entry: './src/index.tsx', 7 | output: { 8 | filename: 'receipt.js', 9 | }, 10 | resolve: { 11 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.css', '.scss'], 12 | alias: { 13 | src: path.resolve(__dirname, './src'), 14 | testConfig: path.resolve(__dirname, './testConfig'), 15 | }, 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.jsx?/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader', 24 | }, 25 | }, 26 | { 27 | test: /\.scss$/, 28 | use: ['style-loader', 'css-loader'], 29 | }, 30 | { 31 | test: /\.css$/, 32 | use: [ 33 | { 34 | loader: MiniCssExtractPlugin.loader, 35 | }, 36 | { 37 | loader: 'css-loader', 38 | options: { 39 | url: false, 40 | }, 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | plugins: [ 47 | new ForkTsCheckerWebpackPlugin(), 48 | new MiniCssExtractPlugin({ 49 | filename: 'receipt.css', 50 | }), 51 | ], 52 | }; 53 | -------------------------------------------------------------------------------- /src/frontend/src/components/AltinnIcon.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@material-ui/core'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import classNames from 'classnames'; 4 | import React from 'react'; 5 | import altinnTheme from 'src/theme/altinnStudioTheme'; 6 | 7 | export interface IAltinnIconCompontentProvidedProps { 8 | iconClass: string; 9 | isActive?: boolean; 10 | isActiveIconColor?: string; 11 | iconColor: any; 12 | iconSize?: number|string; 13 | padding?: string; 14 | margin?: string; 15 | weight?: number; 16 | } 17 | 18 | const theme = createTheme(altinnTheme); 19 | 20 | const styles = { 21 | activeIcon: { 22 | color: theme.altinnPalette.primary.blueDark, 23 | }, 24 | }; 25 | 26 | export class AltinnIcon extends React.Component { 27 | public render() { 28 | return ( 29 | 42 | ); 43 | } 44 | } 45 | 46 | export default withStyles(styles)(AltinnIcon); 47 | -------------------------------------------------------------------------------- /test/selfSignedTestCertificatePublic.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID/zCCAuegAwIBAgIQF2ov3ZZUmJVKtoz0a1fabDANBgkqhkiG9w0BAQsFADB/ 3 | MRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHY29udG9zbzEU 4 | MBIGCgmSJomT8ixkARkWBGNvcnAxFTATBgNVBAsMDFVzZXJBY2NvdW50czEiMCAG 5 | A1UEAwwZQWx0aW5uIFBsYXRmb3JtIFVuaXQgdGVzdDAgFw0yMDA0MTQwOTMwMTda 6 | GA8yMTIwMDQxNDA5NDAxOFowfzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS 7 | JomT8ixkARkWB2NvbnRvc28xFDASBgoJkiaJk/IsZAEZFgRjb3JwMRUwEwYDVQQL 8 | DAxVc2VyQWNjb3VudHMxIjAgBgNVBAMMGUFsdGlubiBQbGF0Zm9ybSBVbml0IHRl 9 | c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCAKc+q5jbYFyQFxM1 10 | xU3v0N477ppnMu03K8qlEkX0+yffRHcR1I0Kku8yg1S+LQjeqh1K42b270myKiIt 11 | vxeuNnanRwdehTZthThembr8RXoGcmzaXfMet7NVDgUa7gNzPXbqjhTFdyWoZzeU 12 | X6TWTgFtciTs5M1F50H+3nieGKX2dvLUIEXWFO7yevj9bqtI8k0b66eLgBjchnjW 13 | 8B7oYOFZW44VDDnqQrvFJ9aMQ44FfLAWWLcy6nBzcDdK+Z+yq9FNVgduyl0J7vRo 14 | 3UtcVazLUvmDdwASLIB3IwB7YmT6fuOyM+6eyw5F1CdjXbc/bhop0pCDY1aAEsZA 15 | CjT9AgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD 16 | AjAtBgNVHREEJjAkoCIGCisGAQQBgjcUAgOgFAwSdGVzdEBhbHRpbm4uc3R1ZGlv 17 | MB0GA1UdDgQWBBTv8Cpf5J7nfmGds20LU/J3bg05XTANBgkqhkiG9w0BAQsFAAOC 18 | AQEAahWeu6ymaiJe9+LiMlQwNsUIV4KaLX+jCsRyF1jUJ0C13aFALGM4k9svqqXR 19 | DzBdCXXr0c1E+Ks3sCwBLfK5yj5fTI+pL26ceEmHahcVyLvzEBljtNb4FnGFs92P 20 | CH0NuCz45hQ2O9/Tv4cZAdgledTznJTKzzQNaF8M6iINmP6sf4kOg0BQx0K71K4f 21 | 7j2oQvYKiT7Zv1e83cdk9pS4ihDe+ZWYiGUM/IuaXNPl6OzVk4rY88PZJAoz7q33 22 | rYjlT+zkcl3dzTc3E0CWzbIWjhaXCRWvlI44cLRtdpmPqJUHI6a/tcGwNb5vWiT4 23 | YfZJ0EZ2iSRQlpU3+jMs8Ci2AA== 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Configuration/PlatformSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Altinn.Platform.Receipt.Configuration; 2 | 3 | /// 4 | /// Configuration for platform settings 5 | /// 6 | public class PlatformSettings 7 | { 8 | /// 9 | /// Gets or sets the url for the API Authentication endpoint 10 | /// 11 | public string ApiAuthenticationEndpoint { get; set; } 12 | 13 | /// 14 | /// Gets or sets the url for the API profile endpoint 15 | /// 16 | public string ApiProfileEndpoint { get; set; } 17 | 18 | /// 19 | /// Gets or sets the url for the API register endpoint 20 | /// 21 | public string ApiRegisterEndpoint { get; set; } 22 | 23 | /// 24 | /// Gets or sets the url for the API storage endpoint 25 | /// 26 | public string ApiStorageEndpoint { get; set; } 27 | 28 | /// 29 | /// Gets or sets the subscription key value to use in requests against the platform. 30 | /// A new subscription key is generated automatically every time an app is deployed to an environment. The new key is then automatically 31 | /// added to the environment for the app code during deploy. This will override the value stored in app settings. 32 | /// 33 | public string SubscriptionKey { get; set; } 34 | 35 | /// 36 | /// The name of the subscription header for Api management. 37 | /// 38 | public string SubscriptionKeyHeaderName { get; set; } = "Ocp-Apim-Subscription-Key"; 39 | } 40 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Helpers/PlatformHttpException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace Altinn.Platform.Receipt.Helpers 6 | { 7 | /// 8 | /// Exception class to hold exceptions when talking to the platform REST services 9 | /// 10 | public class PlatformHttpException : Exception 11 | { 12 | /// 13 | /// Responsible for holding an http request exception towards platform. 14 | /// 15 | public HttpResponseMessage Response { get; } 16 | 17 | /// 18 | /// Copy the response for further investigations 19 | /// 20 | /// the response 21 | /// the message 22 | public PlatformHttpException(HttpResponseMessage response, string message) : base(message) 23 | { 24 | Response = response; 25 | } 26 | 27 | /// 28 | /// Creates a platform exception 29 | /// 30 | /// The http response 31 | /// A PlatformHttpException 32 | public static async Task CreateAsync(HttpResponseMessage response) 33 | { 34 | string content = await response.Content?.ReadAsStringAsync(); 35 | string message = $"{(int)response.StatusCode} - {response.ReasonPhrase} - {content}"; 36 | 37 | return new PlatformHttpException(response, message); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/frontend/src/utils/instance.test.tsx: -------------------------------------------------------------------------------- 1 | import { getInstanceId, getInstanceOwnerId, getArchiveRef } from './instance'; 2 | 3 | import { mockLocation } from 'testConfig/testUtils'; 4 | 5 | const originalLocation = window.location; 6 | 7 | // instanceId and instanceOwnerId are set in package.json under "jest.testURL" 8 | describe('utils/instance', () => { 9 | beforeEach(() => { 10 | mockLocation(originalLocation); 11 | }); 12 | describe('getInstanceId', () => { 13 | it('should return instanceOwnerId when it exists in url', () => { 14 | expect(getInstanceId()).toEqual('6697de17-18c7-4fb9-a428-d6a414a797ae'); 15 | }); 16 | 17 | it('should return empty string when window.location.pathname is not set', () => { 18 | mockLocation({ pathname: undefined }); 19 | expect(getInstanceId()).toEqual(''); 20 | }); 21 | }); 22 | 23 | describe('getInstanceOwnerId', () => { 24 | it('should return instanceOwnerId when it exists in url', () => { 25 | expect(getInstanceOwnerId()).toEqual('mockInstanceOwnerId'); 26 | }); 27 | 28 | it('should return empty string when window.location.pathname is not set', () => { 29 | mockLocation({ pathname: undefined }); 30 | expect(getInstanceOwnerId()).toEqual(''); 31 | }); 32 | }); 33 | 34 | describe('getArchiveRef', () => { 35 | it('should return last part of instanceId', () => { 36 | expect(getArchiveRef()).toEqual('d6a414a797ae'); 37 | }); 38 | 39 | it('should return undefined when no instanceId is set', () => { 40 | mockLocation({ pathname: undefined }); 41 | 42 | expect(getArchiveRef()).toEqual(undefined); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.21-alpine3.21@sha256:af8023ec879993821f6d5b21382ed915622a1b0f1cc03dbeb6804afaf01f8885 AS build-receipt-frontend 2 | 3 | WORKDIR /build 4 | 5 | COPY src/frontend/package.json . 6 | COPY src/frontend/yarn.lock . 7 | COPY src/frontend/.yarn/ ./.yarn/ 8 | COPY src/frontend/.yarnrc.yml . 9 | 10 | COPY src/frontend/ ./ 11 | 12 | # Install 13 | RUN corepack enable 14 | RUN yarn --immutable 15 | 16 | # Build runtime 17 | RUN yarn run build 18 | 19 | 20 | FROM mcr.microsoft.com/dotnet/sdk:9.0.306-alpine3.22@sha256:f271ed7d0fd9c5a7ed0acafed8a2bc978bb65c19dcd2eeea0415adef142ffc87 AS build 21 | 22 | # Copy receipt backend 23 | WORKDIR /Receipt/ 24 | 25 | COPY src/backend/Altinn.Receipt . 26 | 27 | # Build and publish 28 | RUN dotnet build Altinn.Platform.Receipt.csproj -c Release -o /app_output 29 | RUN dotnet publish Altinn.Platform.Receipt.csproj -c Release -o /app_output 30 | 31 | 32 | FROM mcr.microsoft.com/dotnet/aspnet:9.0.10-alpine3.22@sha256:5e8dca92553951e42caed00f2568771b0620679f419a28b1335da366477d7f98 AS final 33 | EXPOSE 5060 34 | WORKDIR /app 35 | COPY --from=build /app_output . 36 | COPY --from=build-receipt-frontend /build/dist/receipt.js ./wwwroot/receipt/js/react/receipt.js 37 | COPY --from=build-receipt-frontend /build/dist/receipt.css ./wwwroot/receipt/css/receipt.css 38 | 39 | # setup the user and group 40 | # the user will have no password, using shell /bin/false and using the group dotnet 41 | RUN addgroup -g 3000 dotnet && adduser -u 1000 -G dotnet -D -s /bin/false dotnet 42 | # update permissions of files if neccessary before becoming dotnet user 43 | USER dotnet 44 | RUN mkdir /tmp/logtelemetry 45 | 46 | ENTRYPOINT ["dotnet", "Altinn.Platform.Receipt.dll"] 47 | -------------------------------------------------------------------------------- /test/Health/HealthCheckTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | using Altinn.Platform.Receipt.Health; 6 | 7 | using Microsoft.AspNetCore.Mvc.Testing; 8 | using Microsoft.AspNetCore.TestHost; 9 | 10 | using Xunit; 11 | 12 | namespace Altinn.Platform.Receipt.UnitTest; 13 | 14 | /// 15 | /// Health check 16 | /// 17 | public class HealthCheckTests : IClassFixture> 18 | { 19 | private readonly WebApplicationFactory _factory; 20 | 21 | /// 22 | /// Default constructor 23 | /// 24 | /// The web applicaiton factory 25 | public HealthCheckTests(WebApplicationFactory factory) 26 | { 27 | _factory = factory; 28 | } 29 | 30 | /// 31 | /// Verify that component responds on health check 32 | /// 33 | /// 34 | [Fact] 35 | public async Task VerifyHeltCheck_OK() 36 | { 37 | HttpClient client = GetTestClient(); 38 | 39 | HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "/health") 40 | { 41 | }; 42 | 43 | HttpResponseMessage response = await client.SendAsync(httpRequestMessage); 44 | await response.Content.ReadAsStringAsync(); 45 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 46 | } 47 | 48 | private HttpClient GetTestClient() 49 | { 50 | HttpClient client = _factory.WithWebHostBuilder(builder => 51 | { 52 | builder.ConfigureTestServices(services => 53 | { 54 | }); 55 | }).CreateClient(); 56 | 57 | return client; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/frontend/testConfig/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { http } from 'msw'; 2 | import { setupServer } from 'msw/node'; 3 | 4 | import { instance, altinnOrgs, currentUser, application, texts } from './apiResponses'; 5 | 6 | export const mockLocation = (location: object = {}) => { 7 | jest.spyOn(window, 'location', 'get').mockReturnValue({ 8 | ...window.location, 9 | ...location, 10 | }); 11 | }; 12 | 13 | export const instanceHandler = (response: any) => { 14 | return http.get( 15 | 'https://platform.at22.altinn.cloud/receipt/api/v1/instances/mockInstanceOwnerId/6697de17-18c7-4fb9-a428-d6a414a797ae', 16 | () => new Response(JSON.stringify(response)), 17 | ); 18 | }; 19 | 20 | export const textsHandler = (response: any) => { 21 | return http.get( 22 | 'https://localhost/storage/api/v1/applications/ttd/frontend-test/texts/nb', 23 | () => new Response(JSON.stringify(response)), 24 | ); 25 | }; 26 | 27 | export const handlers: any = [ 28 | instanceHandler(instance), 29 | textsHandler(texts), 30 | 31 | http.get('https://altinncdn.no/orgs/altinn-orgs.json', () => new Response(JSON.stringify(altinnOrgs))), 32 | http.get('https://localhost/receipt/api/v1/users/current', () => new Response(JSON.stringify(currentUser))), 33 | http.get( 34 | 'https://localhost/receipt/api/v1/users/current/language', 35 | () => new Response(JSON.stringify({ language: 'nb' })), 36 | ), 37 | http.get( 38 | 'https://localhost/receipt/api/v1/application/attachmentgroupstohide', 39 | () => new Response(JSON.stringify({ attachmentgroupstohide: null })), 40 | ), 41 | http.get( 42 | 'https://platform.at22.altinn.cloud/storage/api/v1/applications/ttd/frontend-test', 43 | () => new Response(JSON.stringify(application)), 44 | ), 45 | ]; 46 | 47 | export { setupServer }; 48 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "PlaceholderCompany" 12 | }, 13 | "orderingRules": { 14 | "usingDirectivesPlacement": "outsideNamespace", 15 | "systemUsingDirectivesFirst": true, 16 | "blankLinesBetweenUsingGroups": "allow" 17 | }, 18 | "namingRules": { 19 | "allowCommonHungarianPrefixes": true, 20 | "allowedHungarianPrefixes": [ 21 | "as", 22 | "d", 23 | "db", 24 | "dn", 25 | "do", 26 | "dr", 27 | "ds", 28 | "dt", 29 | "e", 30 | "e2", 31 | "er", 32 | "f", 33 | "fs", 34 | "go", 35 | "id", 36 | "if", 37 | "in", 38 | "ip", 39 | "is", 40 | "js", 41 | "li", 42 | "my", 43 | "no", 44 | "ns", 45 | "on", 46 | "or", 47 | "pi", 48 | "pv", 49 | "sa", 50 | "sb", 51 | "se", 52 | "si", 53 | "so", 54 | "sp", 55 | "tc", 56 | "to", 57 | "tr", 58 | "ui", 59 | "un", 60 | "wf", 61 | "ws", 62 | "x", 63 | "y", 64 | "j", 65 | "js" 66 | ] 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/frontend-test-build.yml: -------------------------------------------------------------------------------- 1 | name: Frontend Test and Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'src/frontend/**' 8 | - '.github/workflows/frontend-test-build.yml' 9 | pull_request: 10 | paths: 11 | - 'src/frontend/**' 12 | - '.github/workflows/frontend-test-build.yml' 13 | types: [opened, synchronize, reopened] 14 | workflow_dispatch: 15 | 16 | workflow_call: 17 | 18 | jobs: 19 | test-and-build: 20 | name: Test and Build Frontend 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | 25 | defaults: 26 | run: 27 | working-directory: src/frontend 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 35 | with: 36 | node-version: '20' 37 | cache: 'yarn' 38 | cache-dependency-path: 'src/frontend/yarn.lock' 39 | 40 | - name: Install dependencies 41 | run: yarn install --immutable 42 | 43 | - name: Lint 44 | run: yarn lint 45 | 46 | - name: Run tests 47 | run: yarn test --coverage 48 | 49 | - name: Build 50 | run: yarn build 51 | 52 | - name: Upload test coverage 53 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 54 | with: 55 | name: frontend-coverage 56 | path: src/frontend/coverage 57 | 58 | - name: Upload build artifacts 59 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 60 | with: 61 | name: frontend-build 62 | path: src/frontend/dist 63 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | // ACTION REQUIRED: This file was automatically added to your project, but it 3 | // will not take effect until additional steps are taken to enable it. See the 4 | // following page for additional information: 5 | // 6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md 7 | 8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 9 | "settings": { 10 | "documentationRules": { 11 | "companyName": "PlaceholderCompany" 12 | }, 13 | "orderingRules": { 14 | "usingDirectivesPlacement": "outsideNamespace", 15 | "systemUsingDirectivesFirst": true, 16 | "blankLinesBetweenUsingGroups": "allow" 17 | }, 18 | "namingRules": { 19 | "allowCommonHungarianPrefixes": true, 20 | "allowedHungarianPrefixes": [ 21 | "as", 22 | "d", 23 | "db", 24 | "dn", 25 | "do", 26 | "dr", 27 | "ds", 28 | "dt", 29 | "e", 30 | "e2", 31 | "er", 32 | "f", 33 | "fs", 34 | "go", 35 | "id", 36 | "if", 37 | "in", 38 | "ip", 39 | "is", 40 | "js", 41 | "li", 42 | "my", 43 | "no", 44 | "ns", 45 | "on", 46 | "or", 47 | "pi", 48 | "pv", 49 | "sa", 50 | "sb", 51 | "se", 52 | "si", 53 | "so", 54 | "sp", 55 | "tc", 56 | "to", 57 | "tr", 58 | "ui", 59 | "un", 60 | "wf", 61 | "ws", 62 | "x", 63 | "y", 64 | "j", 65 | "js" 66 | ] 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/Mocks/JwtTokenMock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.Security.Claims; 4 | using System.Security.Cryptography.X509Certificates; 5 | 6 | using Microsoft.IdentityModel.Tokens; 7 | 8 | namespace Altinn.Platform.Receipt.Tests.Mocks 9 | { 10 | /// 11 | /// Represents a mechanism for creating JSON Web tokens for use in integration tests. 12 | /// 13 | public static class JwtTokenMock 14 | { 15 | /// 16 | /// Generates a token with a self signed certificate included in the integration test project. 17 | /// 18 | /// The claims principal to include in the token. 19 | /// A new token. 20 | public static string GenerateToken(ClaimsPrincipal principal) 21 | { 22 | JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); 23 | SecurityTokenDescriptor tokenDescriptor = new SecurityTokenDescriptor 24 | { 25 | Subject = new ClaimsIdentity(principal.Identity), 26 | Expires = DateTime.UtcNow.AddSeconds(3600), 27 | SigningCredentials = GetSigningCredentials(), 28 | Audience = "altinn.no" 29 | }; 30 | 31 | SecurityToken token = tokenHandler.CreateToken(tokenDescriptor); 32 | string tokenstring = tokenHandler.WriteToken(token); 33 | 34 | return tokenstring; 35 | } 36 | 37 | private static SigningCredentials GetSigningCredentials() 38 | { 39 | X509Certificate2 cert = X509CertificateLoader.LoadPkcs12FromFile("selfSignedTestCertificate.pfx", "qwer1234"); 40 | return new X509SigningCredentials(cert, SecurityAlgorithms.RsaSha256); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2020": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:import/recommended", 11 | "plugin:import/typescript", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:jsx-a11y/recommended", 14 | "plugin:react/recommended", 15 | "plugin:react-hooks/recommended", 16 | "prettier" 17 | ], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "ecmaFeatures": { 21 | "jsx": true 22 | }, 23 | "sourceType": "module", 24 | "project": "**/tsconfig.json" 25 | }, 26 | "plugins": ["@typescript-eslint", "import"], 27 | "settings": { 28 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"], 29 | "import/parsers": { 30 | "@typescript-eslint/parser": [".ts", ".tsx"] 31 | }, 32 | "react": { 33 | "version": "detect" 34 | } 35 | }, 36 | "ignorePatterns": ["node_modules", "coverage", "**/*.snap"], 37 | "rules": { 38 | "no-console": [ 39 | "warn", 40 | { 41 | "allow": ["warn", "error"] 42 | } 43 | ], 44 | "react/jsx-filename-extension": [ 45 | "error", 46 | { 47 | "extensions": [".jsx", ".tsx"] 48 | } 49 | ], 50 | "jsx-a11y/no-autofocus": ["off"], 51 | "@typescript-eslint/no-explicit-any": ["off"], 52 | "import/no-unresolved": ["off"], 53 | "no-unused-vars": "off", // Disable the base rule 54 | "@typescript-eslint/no-unused-vars": [ 55 | "error", 56 | { 57 | "args": "all", 58 | "argsIgnorePattern": "^_", 59 | "varsIgnorePattern": "^_", 60 | "caughtErrors": "all", 61 | "caughtErrorsIgnorePattern": "^_", 62 | "destructuredArrayIgnorePattern": "^_", 63 | "ignoreRestSiblings": true 64 | } 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/Mocks/ConfigurationManagerStub.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography.X509Certificates; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using Microsoft.IdentityModel.Protocols; 8 | using Microsoft.IdentityModel.Protocols.OpenIdConnect; 9 | using Microsoft.IdentityModel.Tokens; 10 | 11 | namespace Altinn.Platform.Receipt.Tests.Mocks 12 | { 13 | /// 14 | /// Represents a stub of to be used in integration tests. 15 | /// 16 | public class ConfigurationManagerStub : IConfigurationManager 17 | { 18 | /// 19 | public async Task GetConfigurationAsync(CancellationToken cancel) 20 | { 21 | ICollection signingKeys = await GetSigningKeys(); 22 | 23 | OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(); 24 | foreach (var securityKey in signingKeys) 25 | { 26 | configuration.SigningKeys.Add(securityKey); 27 | } 28 | 29 | return configuration; 30 | } 31 | 32 | /// 33 | public void RequestRefresh() 34 | { 35 | throw new NotImplementedException(); 36 | } 37 | 38 | private static async Task> GetSigningKeys() 39 | { 40 | List signingKeys = new List(); 41 | 42 | X509Certificate2 cert = X509CertificateLoader.LoadCertificateFromFile("selfSignedTestCertificatePublic.cer"); 43 | SecurityKey key = new X509SecurityKey(cert); 44 | 45 | signingKeys.Add(key); 46 | 47 | return await Task.FromResult(signingKeys); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/frontend/src/utils/receiptUrlHelper.ts: -------------------------------------------------------------------------------- 1 | import { getInstanceId, getInstanceOwnerId } from './instance'; 2 | 3 | export const altinnAt22PlatformUrl = 'https://platform.at22.altinn.cloud/'; 4 | export const altinnAt22Url = 'https://at22.altinn.cloud/'; 5 | export const altinnOrganisationsUrl = 6 | 'https://altinncdn.no/orgs/altinn-orgs.json'; 7 | 8 | export function getExtendedInstanceUrl() { 9 | return `${getAltinnCloudUrl()}receipt/api/v1/instances/${getInstanceOwnerId()}/${getInstanceId()}?includeParty=true`; 10 | } 11 | 12 | export function getAltinnCloudUrl() { 13 | if ( 14 | window.location.hostname === 'localhost' || 15 | window.location.hostname === '127.0.0.1' || 16 | window.location.hostname === 'altinn3.no' 17 | ) { 18 | // if we are developing locally, point to test data in at22 19 | return altinnAt22PlatformUrl; 20 | } 21 | 22 | // Point to origin. Can be multiple environments. 23 | return `${window.location.origin}/`; 24 | } 25 | 26 | export function getApplicationMetadataUrl(org: string, app: string) { 27 | return `${getAltinnCloudUrl()}storage/api/v1/applications/${org}/${app}`; 28 | } 29 | 30 | export function getUserUrl() { 31 | return `${window.location.origin}/receipt/api/v1/users/current`; 32 | } 33 | 34 | export function getUserLanguageUrl() { 35 | return `${window.location.origin}/receipt/api/v1/users/current/language`; 36 | } 37 | 38 | export function getTextResourceUrl(org: string, app: string, language: string) { 39 | return `${window.location.origin}/storage/api/v1/applications/${org}/${app}/texts/${language}`; 40 | } 41 | 42 | export function getAltinnUrl() { 43 | if (window.location.hostname === 'localhost') { 44 | return altinnAt22Url; 45 | } 46 | return `${window.location.origin}/`; 47 | } 48 | 49 | export function getAttachmentGroupingsToHide() { 50 | return `${window.location.origin}/receipt/api/v1/application/attachmentgroupstohide`; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/frontend/src/components/molecules/AltinnSummaryTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Table, TableBody, TableRow, TableCell, Typography } from '@material-ui/core'; 3 | import classNames from 'classnames'; 4 | import { makeStyles } from '@material-ui/styles'; 5 | 6 | const returnGridRow = (name: string, prop: string, classes: any, index: number) => { 7 | return ( 8 | 14 | 20 | 21 | {name}: 22 | 23 | 24 | 30 | 31 | {prop} 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | const useStyles = makeStyles({ 39 | instanceMetaData: { 40 | marginTop: 36, 41 | }, 42 | tableCell: { 43 | borderBottom: 0, 44 | paddingRight: '2.5rem', 45 | }, 46 | tableRow: { 47 | height: 'auto', 48 | }, 49 | }); 50 | 51 | export interface IAltinnSummaryTableProps { 52 | summaryDataObject: any; 53 | } 54 | 55 | export default function AltinnSummaryTable(props: IAltinnSummaryTableProps) { 56 | const classes = useStyles(); 57 | return ( 58 | 63 | 64 | {Object.keys(props.summaryDataObject).map((name, i) => ( 65 | returnGridRow(name, props.summaryDataObject[name], classes, i) 66 | ))} 67 | 68 |
69 | ) 70 | } -------------------------------------------------------------------------------- /test/HttpClientAccessorTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using Altinn.Platform.Receipt.Clients; 3 | using Altinn.Platform.Receipt.Configuration; 4 | 5 | using Microsoft.Extensions.Options; 6 | 7 | using Xunit; 8 | 9 | namespace Altinn.Platform.Receipt.Tests; 10 | 11 | public class HttpClientAccessorTest 12 | { 13 | private HttpClientAccessor _httpClientAccessor; 14 | private readonly string registerEndpoint = "http://registerendpoint.com/api/v1/"; 15 | private readonly string profileEndpoint = "http://profileendpoint.com/api/v1/"; 16 | private readonly string storageEndpoint = "http://storageendpoint.com/api/v1/"; 17 | private readonly string subscriptionKey = "72D7CAD7-1B89-4940-A0E4-64C2196DBCB8"; 18 | 19 | [Fact] 20 | public void TC01_InstantiateClients_ValidateParameters() 21 | { 22 | // Arrange 23 | _httpClientAccessor = new HttpClientAccessor(Options.Create( 24 | new PlatformSettings 25 | { 26 | ApiProfileEndpoint = profileEndpoint, 27 | ApiRegisterEndpoint = registerEndpoint, 28 | ApiStorageEndpoint = storageEndpoint, 29 | SubscriptionKey = subscriptionKey 30 | })); 31 | 32 | // Act 33 | HttpClient profileClient = _httpClientAccessor.ProfileClient; 34 | HttpClient registerClient = _httpClientAccessor.RegisterClient; 35 | HttpClient storageClient = _httpClientAccessor.StorageClient; 36 | 37 | // Assert 38 | Assert.NotNull(profileClient); 39 | Assert.NotNull(registerClient); 40 | Assert.NotNull(storageClient); 41 | Assert.Equal(profileEndpoint, profileClient.BaseAddress.ToString()); 42 | Assert.Equal(registerEndpoint, registerClient.BaseAddress.ToString()); 43 | Assert.Equal(storageEndpoint, storageClient.BaseAddress.ToString()); 44 | Assert.True(profileClient.DefaultRequestHeaders.Contains("Ocp-Apim-Subscription-Key")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Views/Receipt/receipt.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | @* The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags *@ 9 | 10 | Kvittering 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | @{ 23 | // Comment this link in when running locally 24 | // 25 | } 26 | @* Comment this link out when running locally *@ 27 | 28 | 29 | 30 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | @{ 47 | // Comment this script in when running locally 48 | // 49 | } 50 | @* Comment this script out when running locally *@ 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'src/**' 8 | - '.github/workflows/**' 9 | pull_request: 10 | branches: [main] 11 | types: [opened, synchronize, reopened] 12 | paths: 13 | - 'src/**' 14 | - '.github/workflows/**' 15 | schedule: 16 | - cron: '18 22 * * 3' 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | permissions: 23 | actions: read 24 | contents: read 25 | security-events: write 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | include: 31 | - language: actions 32 | build-mode: none 33 | - language: csharp 34 | build-mode: autobuild 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 38 | - name: Setup .NET 9.0.* SDK 39 | if: matrix.language == 'csharp' 40 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 41 | with: 42 | dotnet-version: | 43 | 9.0.x 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 46 | with: 47 | languages: ${{ matrix.language }} 48 | build-mode: ${{ matrix.build-mode }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | - name: Perform CodeQL Analysis 55 | uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 56 | with: 57 | category: '/language:${{matrix.language}}' 58 | -------------------------------------------------------------------------------- /Altinn.Receipt.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32616.157 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Platform.Receipt.Tests", "test\Altinn.Platform.Receipt.Tests.csproj", "{BFF2B21B-629E-4952-A021-F7CDCB3DAF08}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Altinn.Platform.Receipt", "src\backend\Altinn.Receipt\Altinn.Platform.Receipt.csproj", "{3E337964-095D-467D-ABD3-964798848144}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution items", "{3D25BA0D-131C-4EAB-9EB8-FF968F51D391}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | Dockerfile = Dockerfile 14 | EndProjectSection 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {BFF2B21B-629E-4952-A021-F7CDCB3DAF08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {BFF2B21B-629E-4952-A021-F7CDCB3DAF08}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {BFF2B21B-629E-4952-A021-F7CDCB3DAF08}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {BFF2B21B-629E-4952-A021-F7CDCB3DAF08}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {3E337964-095D-467D-ABD3-964798848144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {3E337964-095D-467D-ABD3-964798848144}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {3E337964-095D-467D-ABD3-964798848144}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {3E337964-095D-467D-ABD3-964798848144}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {3AD03C8F-E4D6-4CEB-AF7E-0166B03F718E} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/frontend/src/theme/altinnAppTheme.tsx: -------------------------------------------------------------------------------- 1 | import { commonTheme } from './commonTheme'; 2 | 3 | const AltinnAppTheme = { 4 | ...commonTheme, 5 | overrides: { 6 | MuiToolbar: { 7 | regular: { 8 | '@media (min-width: 600px)': { 9 | minHeight: 55, 10 | }, 11 | }, 12 | }, 13 | MuiTypography: { 14 | h1: { 15 | fontSize: 36, 16 | }, 17 | h2: { 18 | fontSize: 28, 19 | }, 20 | h3: { 21 | fontSize: 20, 22 | }, 23 | body1: { 24 | fontSize: 16, 25 | }, 26 | body2: { 27 | fontSize: 14, 28 | }, 29 | caption: { 30 | fontSize: 14, 31 | }, 32 | subtitle1: { 33 | fontSize: 14, 34 | }, 35 | }, 36 | MuiPickersToolbar: { 37 | toolbar: { 38 | backgroundColor: '#022F51', 39 | height: '96px', 40 | }, 41 | }, 42 | MuiPickersToolbarText: { 43 | toolbarTxt: { 44 | color: '#fff', 45 | }, 46 | }, 47 | MuiPickersCalendarHeader: { 48 | dayLabel: { 49 | color: '#6A6A6A', 50 | }, 51 | }, 52 | MuiPickersDay: { 53 | daySelected: { 54 | backgroundColor: '#022F51', 55 | }, 56 | }, 57 | }, 58 | props: { 59 | MuiButtonBase: { 60 | disableRipple: true, 61 | disableTouchRipple: true, 62 | }, 63 | }, 64 | sharedStyles: { 65 | boxShadow: '0px 0px 4px rgba(0, 0, 0, 0.25)', 66 | linkBorderBottom: '1px solid #0062BA', 67 | mainPaddingLeft: 73, 68 | fontWeight: { 69 | medium: 500, 70 | }, 71 | }, 72 | typography: { 73 | htmlFontSize: 16, 74 | useNextVariants: true, 75 | fontFamily: [ 76 | '-apple-system', 77 | 'BlinkMacSystemFont', 78 | 'Altinn-DIN', 79 | '"Segoe UI"', 80 | '"Helvetica Neue"', 81 | 'Arial', 82 | 'sans-serif', 83 | '"Apple Color Emoji"', 84 | '"Segoe UI Emoji"', 85 | '"Segoe UI Symbol"', 86 | ].join(','), 87 | }, 88 | }; 89 | 90 | export default AltinnAppTheme; 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.swp 4 | *.*~ 5 | project.lock.json 6 | .DS_Store 7 | *.pyc 8 | 9 | # Visual Studio Code 10 | .vscode 11 | 12 | # User-specific files 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | build/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | msbuild.log 30 | msbuild.err 31 | msbuild.wrn 32 | 33 | # Visual Studio 2015 34 | .vs/ 35 | 36 | 37 | ######### Built react apps for receipt ########### 38 | src/backend/Altinn.Receipt/wwwroot/receipt/js/react 39 | src/backend/Altinn.Receipt/wwwroot/receipt/css 40 | **/dist/* 41 | 42 | 43 | #https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 44 | 45 | /.pnp.* 46 | /.yarn/* 47 | !/.yarn/patches 48 | !/.yarn/plugins 49 | !/.yarn/releases 50 | !/.yarn/sdks 51 | !/.yarn/versions 52 | 53 | src/frontend/.pnp.* 54 | src/frontend/.yarn/* 55 | !src/frontend/.yarn/patches 56 | !src/frontend/.yarn/plugins 57 | !src/frontend/.yarn/releases 58 | !src/frontend/.yarn/sdks 59 | !src/frontend/.yarn/versions 60 | 61 | .DS_STORE 62 | node_modules 63 | scripts/flow/*/.flowconfig 64 | .flowconfig 65 | *~ 66 | *.pyc 67 | .grunt 68 | _SpecRunner.html 69 | __benchmarks__ 70 | build/ 71 | remote-repo/ 72 | coverage/ 73 | .module-cache 74 | fixtures/dom/public/react-dom.js 75 | fixtures/dom/public/react.js 76 | test/the-files-to-test.generated.js 77 | *.log* 78 | chrome-user-data 79 | *.sublime-project 80 | *.sublime-workspace 81 | .idea 82 | *.iml 83 | .vscode 84 | *.swp 85 | *.swo 86 | 87 | packages/react-devtools-core/dist 88 | packages/react-devtools-extensions/chrome/build 89 | packages/react-devtools-extensions/chrome/*.crx 90 | packages/react-devtools-extensions/chrome/*.pem 91 | packages/react-devtools-extensions/firefox/build 92 | packages/react-devtools-extensions/firefox/*.xpi 93 | packages/react-devtools-extensions/firefox/*.pem 94 | packages/react-devtools-extensions/shared/build 95 | packages/react-devtools-extensions/.tempUserDataDir 96 | packages/react-devtools-inline/dist 97 | packages/react-devtools-shell/dist 98 | packages/react-devtools-timeline/dist 99 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Services/StorageWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Net.Http.Json; 5 | using System.Threading.Tasks; 6 | 7 | using Altinn.Platform.Receipt.Configuration; 8 | using Altinn.Platform.Receipt.Extensions; 9 | using Altinn.Platform.Receipt.Helpers; 10 | using Altinn.Platform.Storage.Interface.Models; 11 | 12 | using AltinnCore.Authentication.Utils; 13 | 14 | using Microsoft.AspNetCore.Http; 15 | using Microsoft.Extensions.Options; 16 | 17 | namespace Altinn.Platform.Receipt.Services.Interfaces 18 | { 19 | /// 20 | /// Wrapper for Altinn Platform Storage services 21 | /// 22 | public class StorageWrapper : IStorage 23 | { 24 | private readonly HttpClient _client; 25 | private readonly IHttpContextAccessor _contextAccessor; 26 | 27 | /// 28 | /// Initializes a new instance of the class 29 | /// 30 | public StorageWrapper(HttpClient httpClient, IHttpContextAccessor httpContextAccessor, IOptions platformSettings) 31 | { 32 | httpClient.BaseAddress = new Uri(platformSettings.Value.ApiStorageEndpoint); 33 | httpClient.DefaultRequestHeaders.Add(platformSettings.Value.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); 34 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 35 | _client = httpClient; 36 | _contextAccessor = httpContextAccessor; 37 | } 38 | 39 | /// 40 | public async Task GetInstance(int instanceOwnerId, Guid instanceGuid) 41 | { 42 | string token = JwtTokenUtil.GetTokenFromContext(_contextAccessor.HttpContext, "AltinnStudioRuntime"); 43 | 44 | string url = $"instances/{instanceOwnerId}/{instanceGuid}"; 45 | 46 | HttpResponseMessage response = await _client.GetAsync(token, url); 47 | 48 | if (response.StatusCode == System.Net.HttpStatusCode.OK) 49 | { 50 | return await response.Content.ReadFromJsonAsync(JsonSerializerOptionsProvider.Options); 51 | } 52 | 53 | throw new PlatformHttpException(response, string.Empty); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/frontend/src/components/AltinnLogo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render as rtlRender, screen } from '@testing-library/react'; 3 | import type { IAltinnLogoProps } from './AltinnLogo'; 4 | import { AltinnLogo } from './AltinnLogo'; 5 | import altinnTheme from '../../src/theme/altinnAppTheme'; 6 | 7 | describe('AltinnLogo', () => { 8 | it('should have black image src and custom color as filter class when passing a custom color string', () => { 9 | render({ color: '#E58F65' }); 10 | 11 | const img = getImage(); 12 | expect(img.src).toContain('Altinn-logo-black.svg'); 13 | expect(img.className).toContain('logo-filter-E58F65'); 14 | }); 15 | 16 | it('should have white image src and no custom color as filter class when passing white as color', () => { 17 | render({ color: 'white' }); 18 | 19 | const img = getImage(); 20 | expect(img.src).toContain('Altinn-logo-white.svg'); 21 | expect(img.className).not.toContain('logo-filter'); 22 | }); 23 | 24 | it('should have white image src and no custom color as filter class when passing white color from theme palette', () => { 25 | render({ color: altinnTheme.altinnPalette.primary.white }); 26 | 27 | const img = getImage(); 28 | expect(img.src).toContain('Altinn-logo-white.svg'); 29 | expect(img.className).not.toContain('logo-filter'); 30 | }); 31 | 32 | it('should have blue image src and no custom color as filter class when passing blueDark as color', () => { 33 | render({ color: 'blueDark' }); 34 | 35 | const img = getImage(); 36 | expect(img.src).toContain('Altinn-logo-blue.svg'); 37 | expect(img.className).not.toContain('logo-filter'); 38 | }); 39 | 40 | it('should have blue image src and no custom color as filter class when passing blueDark color from theme palette', () => { 41 | render({ color: altinnTheme.altinnPalette.primary.blueDark }); 42 | 43 | const img = getImage(); 44 | expect(img.src).toContain('Altinn-logo-blue.svg'); 45 | expect(img.className).not.toContain('logo-filter'); 46 | }); 47 | }); 48 | 49 | const getImage = () => 50 | screen.getByRole('img', { 51 | name: /altinn logo/i, 52 | }) as HTMLImageElement; 53 | 54 | const render = (props: Partial = {}) => { 55 | const allProps = { 56 | color: 'white', 57 | ...props, 58 | }; 59 | 60 | rtlRender(); 61 | }; 62 | -------------------------------------------------------------------------------- /src/frontend/src/theme/commonTheme.tsx: -------------------------------------------------------------------------------- 1 | declare module '@material-ui/core/styles/createTheme' { 2 | // tslint:disable-next-line:interface-name 3 | interface Theme { 4 | accessibility: { 5 | focusVisible: { 6 | border: string, 7 | }, 8 | }; 9 | altinnPalette: { 10 | primary: { 11 | blueDarker: string, 12 | blueDark: string, 13 | blueDarkHover: string, 14 | blueMedium: string, 15 | blue: string, 16 | blueHover: string, 17 | blueLight: string, 18 | blueLighter: string, 19 | green: string, 20 | greenHover: string, 21 | greenLight: string, 22 | red: string, 23 | redLight: string, 24 | purple: string, 25 | purpleLight: string, 26 | yellow: string, 27 | yellowLight: string, 28 | black: string, 29 | grey: string, 30 | greyMedium: string, 31 | greyLight: string, 32 | white: string, 33 | }, 34 | }; 35 | sharedStyles: { 36 | boxShadow: string, 37 | linkBorderBottom: string, 38 | mainPaddingLeft: number, 39 | leftDrawerMenuClosedWidth: number, 40 | }; 41 | } 42 | } 43 | 44 | export const commonTheme = { 45 | accessibility: { 46 | focusVisible: { 47 | border: '2px solid #1eaef7', 48 | }, 49 | }, 50 | altinnPalette: { 51 | primary: { 52 | blueDarker: '#022F51', 53 | blueDark: '#0062BA', 54 | blueDarkHover: '#1A72C1', 55 | blueMedium: '#008FD6', 56 | blue: '#1EADF7', 57 | blueHover: '#37b7f8', 58 | blueLight: '#CFF0FF', 59 | blueLighter: '#E3F7FF', 60 | green: '#12AA64', 61 | greenHover: '#45D489', 62 | greenLight: '#D4F9E4', 63 | red: '#E23B53', 64 | redLight: '#F9CAD3', 65 | purple: '#3F3161', 66 | purpleLight: '#E0DAF7', 67 | yellow: '#FFDA06', 68 | yellowLight: '#FBF6BD', 69 | black: '#000', 70 | grey: '#6a6a6a', 71 | greyMedium: '#BCC7CC', 72 | greyLight: '#EFEFEF', 73 | white: '#FFF', 74 | }, 75 | }, 76 | breakpoints: { 77 | values: { 78 | xs: 0, 79 | sm: 600, 80 | md: 1025, 81 | lg: 1440, 82 | xl: 1920, 83 | }, 84 | }, 85 | palette: { 86 | primary: { 87 | main: '#000', 88 | }, 89 | // Colors that are not part of the altinn color palette but is still used 90 | secondary: { 91 | main: '#000', 92 | dark: '#d2d2d2', 93 | }, 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /test/Altinn.Platform.Receipt.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | BFF2B21B-629E-4952-A021-F7CDCB3DAF08 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | stylecop.json 35 | 36 | 37 | 38 | 39 | 40 | Always 41 | 42 | 43 | Always 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Always 54 | true 55 | PreserveNewest 56 | 57 | 58 | 59 | 60 | true 61 | $(NoWarn);1591 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/LanguageHelperTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Altinn.Platform.Receipt.Helpers.Tests; 4 | 5 | public class LanguageHelperTests 6 | { 7 | [Fact] 8 | public void GetLanguageFromAltinnPersistenceCookie_NullCookie_ReturnsEmptyString() 9 | { 10 | // Arrange 11 | string cookieValue = null; 12 | string expectedLanguage = string.Empty; // Default is empty string 13 | 14 | // Act 15 | string result = LanguageHelper.GetLanguageFromAltinnPersistenceCookie(cookieValue); 16 | 17 | // Assert 18 | Assert.Equal(expectedLanguage, result); 19 | } 20 | 21 | [Fact] 22 | public void GetLanguageFromAltinnPersistenceCookie_ContainsEnglishLanguage_ReturnsEnglish() 23 | { 24 | // Arrange 25 | string cookieValue = "UL=1033"; // Cookie containing English language 26 | string expectedLanguage = "en"; 27 | 28 | // Act 29 | string result = LanguageHelper.GetLanguageFromAltinnPersistenceCookie(cookieValue); 30 | 31 | // Assert 32 | Assert.Equal(expectedLanguage, result); 33 | } 34 | 35 | [Fact] 36 | public void GetLanguageFromAltinnPersistenceCookie_ContainsNorwegianLanguage_ReturnsNorwegian() 37 | { 38 | // Arrange 39 | string cookieValue = "UL=1044"; // Cookie containing Norwegian language 40 | string expectedLanguage = "nb"; 41 | 42 | // Act 43 | string result = LanguageHelper.GetLanguageFromAltinnPersistenceCookie(cookieValue); 44 | 45 | // Assert 46 | Assert.Equal(expectedLanguage, result); 47 | } 48 | 49 | [Fact] 50 | public void GetLanguageFromAltinnPersistenceCookie_ContainsNynorskLanguage_ReturnsNynorsk() 51 | { 52 | // Arrange 53 | string cookieValue = "UL=2068"; // Cookie containing Nynorsk language 54 | string expectedLanguage = "nn"; 55 | 56 | // Act 57 | string result = LanguageHelper.GetLanguageFromAltinnPersistenceCookie(cookieValue); 58 | 59 | // Assert 60 | Assert.Equal(expectedLanguage, result); 61 | } 62 | 63 | [Fact] 64 | public void GetLanguageFromAltinnPersistenceCookie_UnsupportedLanguage_ReturnsEmptyString() 65 | { 66 | // Arrange 67 | string cookieValue = "UL=9999"; // Cookie containing unsupported language 68 | string expectedLanguage = string.Empty; // Default is empty string 69 | 70 | // Act 71 | string result = LanguageHelper.GetLanguageFromAltinnPersistenceCookie(cookieValue); 72 | 73 | // Assert 74 | Assert.Equal(expectedLanguage, result); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Services/ProfileWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Net.Http.Json; 5 | using System.Threading.Tasks; 6 | 7 | using Altinn.Common.AccessTokenClient.Services; 8 | using Altinn.Platform.Profile.Models; 9 | using Altinn.Platform.Receipt.Configuration; 10 | using Altinn.Platform.Receipt.Extensions; 11 | using Altinn.Platform.Receipt.Helpers; 12 | 13 | using AltinnCore.Authentication.Utils; 14 | 15 | using Microsoft.AspNetCore.Http; 16 | using Microsoft.Extensions.Options; 17 | 18 | namespace Altinn.Platform.Receipt.Services.Interfaces 19 | { 20 | /// 21 | /// Wrapper for Altinn Platform Profile services 22 | /// 23 | public class ProfileWrapper : IProfile 24 | { 25 | private readonly HttpClient _client; 26 | private readonly IHttpContextAccessor _contextaccessor; 27 | private readonly IAccessTokenGenerator _accessTokenGenerator; 28 | 29 | /// 30 | /// Initializes a new instance of the class 31 | /// 32 | public ProfileWrapper(HttpClient httpClient, IHttpContextAccessor httpContextAccessor, IOptions platformSettings, IAccessTokenGenerator accessTokenGenerator) 33 | { 34 | httpClient.BaseAddress = new Uri(platformSettings.Value.ApiProfileEndpoint); 35 | httpClient.DefaultRequestHeaders.Add(platformSettings.Value.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); 36 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 37 | _client = httpClient; 38 | _contextaccessor = httpContextAccessor; 39 | _accessTokenGenerator = accessTokenGenerator; 40 | } 41 | 42 | /// 43 | public async Task GetUser(int userId) 44 | { 45 | string token = JwtTokenUtil.GetTokenFromContext(_contextaccessor.HttpContext, "AltinnStudioRuntime"); 46 | string url = $"users/{userId}"; 47 | 48 | HttpResponseMessage response = await _client.GetAsync(token, url, _accessTokenGenerator.GenerateAccessToken("platform", "receipt")); 49 | 50 | if (response.StatusCode == System.Net.HttpStatusCode.OK) 51 | { 52 | return await response.Content.ReadFromJsonAsync(JsonSerializerOptionsProvider.Options); 53 | } 54 | 55 | throw new PlatformHttpException(response, string.Empty); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/frontend/src/utils/receipt.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import type { IAltinnOrgs, IApplication, IInstance, ILanguage, IParty, ITextResource } from 'src/types'; 4 | 5 | import { getCurrentTaskData } from 'src/utils/applicationMetaDataUtils'; 6 | import { getAppReceiver, getLanguageFromKey } from 'src/utils/language'; 7 | 8 | import { getArchiveRef } from './instance'; 9 | 10 | const formatDate = (date: any): string => moment(date).format('DD.MM.YYYY / HH:mm'); 11 | 12 | const getDateSubmitted = (instance: IInstance, application: IApplication): string | undefined => { 13 | if (instance.data && instance.data.length > 0) { 14 | const currentTaskData = getCurrentTaskData(application, instance); 15 | if (currentTaskData !== undefined) { 16 | return instance.process?.ended 17 | ? formatDate(instance.process.ended) 18 | : formatDate(currentTaskData.lastChanged); 19 | } 20 | } 21 | 22 | if (instance.status.isArchived) { 23 | return formatDate(instance.status.archived); 24 | } 25 | 26 | return undefined; 27 | }; 28 | 29 | const getSender = (party: IParty): string => { 30 | if (party.ssn) { 31 | return `${party.ssn}-${party.name}`; 32 | } else if (party.orgNumber) { 33 | return `${party.orgNumber}-${party.name}`; 34 | } 35 | return ''; 36 | }; 37 | 38 | export const getInstanceMetaDataObject = ( 39 | instance: IInstance, 40 | party: IParty, 41 | language: ILanguage, 42 | organisations: IAltinnOrgs, 43 | application: IApplication, 44 | textResources: ITextResource[], 45 | userLanguage: string, 46 | ) => { 47 | const obj = {} as any; 48 | 49 | if (!instance || !party || !language || !organisations) { 50 | return obj; 51 | } 52 | 53 | let dateSubmitted: any = getDateSubmitted(instance, application); 54 | 55 | if (dateSubmitted === undefined && instance.status.isArchived) { 56 | dateSubmitted = moment(instance.status.archived).format('DD.MM.YYYY / HH:mm'); 57 | } 58 | 59 | if (instance.isA2Lookup){ 60 | obj[getLanguageFromKey('receipt_platform.date_archived', language)] = dateSubmitted; 61 | } else { 62 | obj[getLanguageFromKey('receipt_platform.date_sent', language)] = dateSubmitted; 63 | } 64 | 65 | const sender = getSender(party); 66 | 67 | if (!instance.isA2Lookup) { 68 | obj[getLanguageFromKey('receipt_platform.sender', language)] = sender; 69 | obj[getLanguageFromKey('receipt_platform.receiver', language)] = getAppReceiver(textResources, organisations, instance.org, userLanguage); 70 | } 71 | 72 | obj[getLanguageFromKey('receipt_platform.reference_number', language)] = getArchiveRef(); 73 | 74 | return obj; 75 | }; 76 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Services/RegisterWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Net.Http.Json; 5 | using System.Threading.Tasks; 6 | 7 | using Altinn.Common.AccessTokenClient.Services; 8 | using Altinn.Platform.Receipt.Configuration; 9 | using Altinn.Platform.Receipt.Extensions; 10 | using Altinn.Platform.Receipt.Helpers; 11 | using Altinn.Platform.Receipt.Services.Interfaces; 12 | using Altinn.Platform.Register.Models; 13 | 14 | using AltinnCore.Authentication.Utils; 15 | 16 | using Microsoft.AspNetCore.Http; 17 | using Microsoft.Extensions.Options; 18 | 19 | namespace Altinn.Platform.Receipt.Services 20 | { 21 | /// 22 | /// Wrapper for Altinn Platform Register services. 23 | /// 24 | public class RegisterWrapper : IRegister 25 | { 26 | private readonly HttpClient _client; 27 | private readonly IHttpContextAccessor _contextaccessor; 28 | private readonly IAccessTokenGenerator _accessTokenGenerator; 29 | 30 | /// 31 | /// Initializes a new instance of the class 32 | /// 33 | public RegisterWrapper(HttpClient httpClient, IHttpContextAccessor httpContextAccessor, IOptions platformSettings, IAccessTokenGenerator accessTokenGenerator) 34 | { 35 | httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); 36 | httpClient.DefaultRequestHeaders.Add(platformSettings.Value.SubscriptionKeyHeaderName, platformSettings.Value.SubscriptionKey); 37 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 38 | _client = httpClient; 39 | _contextaccessor = httpContextAccessor; 40 | _accessTokenGenerator = accessTokenGenerator; 41 | } 42 | 43 | /// 44 | public async Task GetParty(int partyId) 45 | { 46 | string token = JwtTokenUtil.GetTokenFromContext(_contextaccessor.HttpContext, "AltinnStudioRuntime"); 47 | string url = $"parties/{partyId}"; 48 | 49 | HttpResponseMessage response = await _client.GetAsync(token, url, _accessTokenGenerator.GenerateAccessToken("platform", "receipt")); 50 | 51 | if (response.StatusCode == System.Net.HttpStatusCode.OK) 52 | { 53 | return await response.Content.ReadFromJsonAsync(JsonSerializerOptionsProvider.Options); 54 | } 55 | 56 | throw new PlatformHttpException(response, string.Empty); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Altinn.Platform.Receipt.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | true 6 | 7 | {3E337964-095D-467D-ABD3-964798848144} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | stylecop.json 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/build-and-analyze-fork.yml: -------------------------------------------------------------------------------- 1 | name: Code test and analysis (fork) 2 | on: 3 | pull_request: 4 | branches: [main] 5 | types: [opened, synchronize, reopened, ready_for_review] 6 | jobs: 7 | test: 8 | if: github.actor == 'dependabot[bot]' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) 9 | name: Build and Test 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | steps: 14 | - name: Setup .NET 15 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 16 | with: 17 | dotnet-version: | 18 | 9.0.x 19 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 20 | with: 21 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 22 | 23 | - name: dotnet build 24 | run: dotnet build Altinn.Receipt.sln -v m 25 | 26 | - name: dotnet test 27 | run: dotnet test Altinn.Receipt.sln --results-directory TestResults/ --collect:"XPlat Code Coverage" -v m 28 | 29 | - name: Generate coverage results 30 | run: | 31 | dotnet tool install --global dotnet-reportgenerator-globaltool 32 | reportgenerator -reports:TestResults/**/coverage.cobertura.xml -targetdir:TestResults/Output/CoverageReport -reporttypes:Cobertura 33 | 34 | - name: Archive code coverage results 35 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 36 | with: 37 | name: code-coverage-report 38 | path: TestResults/Output/CoverageReport/ 39 | 40 | code-coverage: 41 | if: github.actor == 'dependabot[bot]' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) 42 | name: Report code coverage 43 | runs-on: ubuntu-latest 44 | permissions: {} 45 | needs: test 46 | steps: 47 | - name: Download Coverage Results 48 | uses: actions/download-artifact@master 49 | with: 50 | name: code-coverage-report 51 | path: dist/ 52 | - name: Create Coverage Summary Report 53 | uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 54 | with: 55 | filename: dist/Cobertura.xml 56 | badge: true 57 | fail_below_min: true 58 | format: markdown 59 | hide_branch_rate: false 60 | hide_complexity: true 61 | indicators: true 62 | output: both 63 | thresholds: '60 80' 64 | 65 | # Step disabled until workaround available for commenting PR 66 | # - name: Add Coverage PR Comment 67 | # uses: marocchino/sticky-pull-request-comment@v2 68 | # with: 69 | # recreate: true 70 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | # path: code-coverage-results.md 72 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Clients/HttpClientAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | 5 | using Altinn.Platform.Receipt.Configuration; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Altinn.Platform.Receipt.Clients 9 | { 10 | /// 11 | /// Http client accessor for accessing clients for Altinn Platform integration 12 | /// 13 | public class HttpClientAccessor : IHttpClientAccessor 14 | { 15 | private readonly PlatformSettings _platformSettings; 16 | 17 | private HttpClient _storageClient; 18 | private HttpClient _registerClient; 19 | private HttpClient _profileClient; 20 | 21 | private const string SubscriptionKeyHeaderName = "Ocp-Apim-Subscription-Key"; 22 | 23 | /// 24 | /// Initialises a new instance of the class with the given platform settings. 25 | /// The platform settings used to configure the HTTP clients. 26 | /// 27 | public HttpClientAccessor(IOptions platformSettings) 28 | { 29 | _platformSettings = platformSettings.Value; 30 | } 31 | 32 | /// 33 | public HttpClient RegisterClient 34 | { 35 | get 36 | { 37 | if (_registerClient != null) 38 | { 39 | return _registerClient; 40 | } 41 | 42 | _registerClient = GetNewHttpClient(_platformSettings.ApiRegisterEndpoint); 43 | return _registerClient; 44 | } 45 | } 46 | 47 | /// 48 | public HttpClient ProfileClient 49 | { 50 | get 51 | { 52 | if (_profileClient != null) 53 | { 54 | return _profileClient; 55 | } 56 | 57 | _profileClient = GetNewHttpClient(_platformSettings.ApiProfileEndpoint); 58 | return _profileClient; 59 | } 60 | } 61 | 62 | /// 63 | public HttpClient StorageClient 64 | { 65 | get 66 | { 67 | if (_storageClient != null) 68 | { 69 | return _storageClient; 70 | } 71 | 72 | _storageClient = GetNewHttpClient(_platformSettings.ApiStorageEndpoint); 73 | return _storageClient; 74 | } 75 | } 76 | 77 | private HttpClient GetNewHttpClient(string apiEndpoint) 78 | { 79 | HttpClient httpClient = new HttpClient 80 | { 81 | BaseAddress = new Uri(apiEndpoint) 82 | }; 83 | 84 | httpClient.DefaultRequestHeaders.Add(SubscriptionKeyHeaderName, _platformSettings.SubscriptionKey); 85 | httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 86 | return httpClient; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/frontend/src/components/organisms/AltinnAppHeader.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AltinnAppHeader } from '..'; 3 | import type { IParty } from 'src/types'; 4 | import { render, screen } from '@testing-library/react'; 5 | import userEvent from '@testing-library/user-event'; 6 | 7 | const user = userEvent.setup(); 8 | 9 | describe('organisms/AltinnAppHeader', () => { 10 | const partyPerson = { 11 | name: 'Test Testesen', 12 | ssn: '01010000000', 13 | partyId: '12345', 14 | } as IParty; 15 | 16 | const partyOrg = { 17 | orgNumber: 12345678, 18 | partyId: '54321', 19 | name: 'Bedrift', 20 | } as IParty; 21 | 22 | const selfIdentifiedUser = { 23 | childParties: null, 24 | isDeleted: false, 25 | name: 'uidp_brxzt8pt992', 26 | onlyHierarchyElementWithNoAccess: false, 27 | orgNumber: null, 28 | organization: null, 29 | partyId: '52057791', 30 | partyTypeName: 3, 31 | person: null, 32 | ssn: 'ssn', 33 | unitType: null, 34 | } as IParty; 35 | 36 | const headerBackgroundColor = 'blue'; 37 | const logoColor = 'blue'; 38 | 39 | const renderComponent = (party: IParty, user = partyPerson) => { 40 | render( 41 | , 49 | ); 50 | }; 51 | 52 | it('should render private icon when party is person', () => { 53 | renderComponent(partyPerson); 54 | const profileButton = screen.getByRole('button', { 55 | name: /profilikon meny/i, 56 | }); 57 | expect(profileButton.firstChild.firstChild).toHaveClass( 58 | 'fa-private-circle-big', 59 | ); 60 | }); 61 | 62 | it('should render private icon for user without ssn or org number', () => { 63 | renderComponent(selfIdentifiedUser); 64 | const profileButton = screen.getByRole('button', { 65 | name: /profilikon meny/i, 66 | }); 67 | expect(profileButton.firstChild.firstChild).toHaveClass( 68 | 'fa-private-circle-big', 69 | ); 70 | }); 71 | 72 | it('should render org icon when party is org', () => { 73 | renderComponent(partyOrg); 74 | const profileButton = screen.getByRole('button', { 75 | name: /profilikon meny/i, 76 | }); 77 | expect(profileButton.firstChild.firstChild).toHaveClass( 78 | 'fa-corp-circle-big', 79 | ); 80 | }); 81 | 82 | it('should render menu with logout option when clicking profile icon', async () => { 83 | renderComponent(partyOrg); 84 | expect( 85 | screen.queryByRole('link', { 86 | name: /logg ut/i, 87 | hidden: true, 88 | }), 89 | ).toBeNull(); 90 | await user.click( 91 | screen.getByRole('button', { 92 | name: /profilikon meny/i, 93 | }), 94 | ); 95 | expect( 96 | screen.getByRole('link', { 97 | name: /logg ut/i, 98 | hidden: true, 99 | }), 100 | ).toBeInTheDocument(); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/frontend/src/components/organisms/AltinnAppHeaderMenu.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, makeStyles, Menu, MenuItem } from '@material-ui/core'; 2 | import React from 'react'; 3 | import { AltinnIcon } from '..'; 4 | import { IParty } from '../../types'; 5 | import { logoutUrlAltinn } from '../../utils/urlHelper'; 6 | 7 | export interface IAltinnAppHeaderMenuProps { 8 | party: IParty; 9 | logoColor: string; 10 | ariaLabel: string; 11 | logoutText: string; 12 | } 13 | 14 | const useStyles = makeStyles({ 15 | paperStyle: { 16 | borderRadius: 1, 17 | maxWidth: 100, 18 | padding: 0, 19 | top: 50, 20 | right: 25, 21 | }, 22 | menuItem: { 23 | fontSize: 16, 24 | justifyContent: 'flex-end', 25 | paddingRight: 25, 26 | }, 27 | iconButton: { 28 | padding: 0, 29 | }, 30 | }); 31 | 32 | function AltinnAppHeaderMenu(props: IAltinnAppHeaderMenuProps) { 33 | const { 34 | party, 35 | logoColor, 36 | ariaLabel, 37 | logoutText, 38 | } = props; 39 | const [anchorEl, setAnchorEl] = React.useState(null); 40 | const classes = useStyles(); 41 | 42 | const handleClick = (event: any) => { 43 | setAnchorEl(event.currentTarget); 44 | }; 45 | 46 | const handleClose = () => { 47 | setAnchorEl(null); 48 | }; 49 | 50 | return ( 51 | <> 52 | 60 | {party && party.ssn && 61 | 67 | } 68 | {party && party.orgNumber && 69 | 75 | } 76 | 77 | 95 | 99 | {// workaround for highlighted menu item not changing. 100 | // https://github.com/mui-org/material-ui/issues/5186#issuecomment-337278330 101 | } 102 | 106 | 107 | {logoutText} 108 | 109 | 110 | 111 | 112 | ); 113 | } 114 | 115 | export default AltinnAppHeaderMenu; 116 | -------------------------------------------------------------------------------- /.github/workflows/update-chart.yml: -------------------------------------------------------------------------------- 1 | name: Update Helm Chart Version and Create Pull Request 2 | 3 | on: 4 | schedule: 5 | - cron: '*/5 * * * *' # Runs every 5 minutes 6 | workflow_dispatch: # Allows manual trigger 7 | 8 | permissions: 9 | id-token: write 10 | contents: write 11 | pull-requests: write 12 | 13 | env: 14 | REGISTRY: ${{ secrets.ALTINN_REGISTRY }} 15 | APP_NAME: altinn-receipt 16 | 17 | jobs: 18 | update-helm-chart: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 24 | 25 | - name: 'Azure login' 26 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 27 | with: 28 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 29 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 30 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 31 | 32 | - name: Get latest Helm chart version from ACR 33 | id: get-latest-version 34 | run: | 35 | latest_version=$(az acr repository show-tags --name ${{ env.REGISTRY }} --repository charts/${{ env.APP_NAME }} --output tsv --orderby time_desc --top 1) 36 | # Replace `_` with `+`, this is due to the oci repo not supporting `+` in tags 37 | echo "latest_version=${latest_version//_/+}" >> $GITHUB_ENV 38 | 39 | - name: Extract current version from YAML 40 | id: extract-current-version 41 | run: | 42 | current_version=$(yq eval '.spec.chart.spec.version' .deploy/app.yaml) 43 | echo "current_version=$current_version" >> $GITHUB_ENV 44 | 45 | - name: Compare versions 46 | id: compare-versions 47 | run: | 48 | if [ "${{ env.latest_version }}" != "${{ env.current_version }}" ]; then 49 | echo "::set-output name=should_update::true" 50 | else 51 | echo "::set-output name=should_update::false" 52 | fi 53 | 54 | - name: Update YAML file 55 | if: steps.compare-versions.outputs.should_update == 'true' 56 | run: | 57 | yq eval '.spec.chart.spec.version = "${{ env.latest_version }}"' -i .deploy/app.yaml 58 | 59 | - name: Create Pull Request 60 | if: steps.compare-versions.outputs.should_update == 'true' 61 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 62 | with: 63 | token: ${{ secrets.GITHUB_TOKEN }} 64 | delete-branch: true 65 | commit-message: 'Update Helm chart to version ${{ env.latest_version }}' 66 | title: 'Update Helm chart to version ${{ env.latest_version }}' 67 | body: | 68 | - The Helm chart has been updated to version ${{ env.latest_version }}. 69 | 70 | Auto-generated by GitHub Actions. 71 | branch: update-helm-chart-${{ env.latest_version }} 72 | 73 | - name: Clean up 74 | if: steps.compare-versions.outputs.should_update == 'true' 75 | run: git checkout main 76 | 77 | - name: Send Trace to Azure Monitor 78 | uses: altinn/altinn-platform/actions/send-ci-cd-trace@cc4b775eb4d7015674bfcaac762e40f270afab87 # v1.0.1 79 | with: 80 | connection_string: ${{ secrets.APP_INSIGHTS_CONNECTION_STRING }} 81 | app: '${{ env.APP_NAME }}' 82 | team: 'core' 83 | repo_token: ${{ secrets.GITHUB_TOKEN }} 84 | -------------------------------------------------------------------------------- /.github/workflows/build-and-analyze.yml: -------------------------------------------------------------------------------- 1 | name: Code test and analysis 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | types: [opened, synchronize, reopened] 8 | workflow_dispatch: 9 | jobs: 10 | build-and-test: 11 | name: Build and Test 12 | if: ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) || github.event_name == 'push') && github.repository_owner == 'Altinn' && github.actor != 'dependabot[bot]' 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | steps: 17 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 18 | - name: Setup .NET 19 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 20 | with: 21 | dotnet-version: | 22 | 9.0.x 23 | - name: Build & Test 24 | run: | 25 | dotnet build Altinn.Receipt.sln -v m 26 | dotnet test Altinn.Receipt.sln -v m 27 | 28 | frontend-test: 29 | uses: ./.github/workflows/frontend-test-build.yml 30 | analyze: 31 | name: Analyze 32 | needs: [build-and-test, frontend-test] 33 | if: ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) || github.event_name == 'push') && github.repository_owner == 'Altinn' && github.actor != 'dependabot[bot]' 34 | runs-on: windows-latest 35 | permissions: 36 | contents: read 37 | steps: 38 | - name: Setup .NET 39 | uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 40 | with: 41 | dotnet-version: | 42 | 9.0.x 43 | - name: Set up JDK 17 44 | uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 45 | with: 46 | distribution: 'microsoft' 47 | java-version: 17 48 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 49 | with: 50 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 51 | - name: Download frontend coverage 52 | uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 53 | with: 54 | name: frontend-coverage 55 | path: src/frontend/coverage/ 56 | - name: Install SonarCloud scanner 57 | shell: powershell 58 | run: | 59 | dotnet tool install --global dotnet-sonarscanner 60 | - name: Analyze 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 63 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 64 | shell: powershell 65 | run: | 66 | dotnet-sonarscanner begin /k:"Altinn_altinn-receipt" /o:"altinn" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vstest.reportsPaths="**/*.trx" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="src/backend/Altinn.Receipt/Program.cs" /d:sonar.javascript.lcov.reportPaths="src/frontend/coverage/lcov.info" /d:sonar.typescript.lcov.reportPaths="src/frontend/coverage/lcov.info" 67 | 68 | dotnet build Altinn.Receipt.sln 69 | dotnet test Altinn.Receipt.sln ` 70 | --no-build ` 71 | --results-directory TestResults/ ` 72 | --collect:"XPlat Code Coverage" ` 73 | -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover 74 | 75 | dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" 76 | -------------------------------------------------------------------------------- /src/frontend/src/components/molecules/AltinnCollapsibleAttachments.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Typography } from '@material-ui/core'; 3 | import Collapse from '@material-ui/core/Collapse'; 4 | import List from '@material-ui/core/List'; 5 | import ListItem from '@material-ui/core/ListItem'; 6 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 7 | import ListItemText from '@material-ui/core/ListItemText'; 8 | import createStyles from '@material-ui/core/styles/createStyles'; 9 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 10 | import classNames from 'classnames'; 11 | import React from 'react'; 12 | import { IAttachment } from '../../types'; 13 | import { AltinnIcon } from '../AltinnIcon'; 14 | import AltinnAttachmentComponent from 'src/components/atoms/AltinnAttachment'; 15 | 16 | const styles = createStyles({ 17 | listItemTextPadding: { 18 | paddingLeft: '0', 19 | }, 20 | transformArrowRight: { 21 | transform: 'rotate(-90deg)', 22 | }, 23 | transition: { 24 | transitionDuration: '0.1s', 25 | minWidth: '0px', 26 | marginRight: '10px' 27 | }, 28 | collapsedTitle: { 29 | fontSize: '20px', 30 | }, 31 | }); 32 | 33 | interface IAltinnCollapsibleAttachmentsProps extends WithStyles { 34 | attachments?: IAttachment[]; 35 | collapsible?: boolean; 36 | title?: React.ReactNode; 37 | hideCount?: boolean; 38 | } 39 | 40 | export function AltinnCollapsibleAttachments(props: IAltinnCollapsibleAttachmentsProps) { 41 | const [open, setOpen] = React.useState(true); 42 | 43 | function handleOpenClose() { 44 | setOpen(!open); 45 | } 46 | 47 | const attachmentCount = props.hideCount == true ? '' 48 | : Array.isArray(props.attachments) ? '(' + props.attachments.length + ')' : '(0)'; 49 | 50 | return( 51 | <> 52 | {props.collapsible ? ( 53 | 57 | 58 | 66 | 71 | 72 | {props.title} {attachmentCount}} 74 | classes={{ 75 | root: classNames(props.classes.listItemTextPadding), 76 | primary: classNames(props.classes.collapsedTitle), 77 | }} 78 | /> 79 | 80 | 81 | 86 | 87 | 88 | ) : ( 89 | <> 90 | 91 | {props.title} {attachmentCount} 92 | 93 | 99 | 100 | 101 | )} 102 | 103 | ); 104 | 105 | } 106 | 107 | export default withStyles(styles)(AltinnCollapsibleAttachments); 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Altinn Platform Receipt 2 | 3 | ## Build status 4 | [![Receipt build status](https://dev.azure.com/brreg/altinn-studio/_apis/build/status/altinn-platform/receipt-master?label=platform/receipt)](https://dev.azure.com/brreg/altinn-studio/_build/latest?definitionId=58) 5 | 6 | ## Getting Started 7 | 8 | These instructions will get you a copy of the receipt component up and running on your machine for development and testing purposes. 9 | 10 | ### Prerequisites 11 | 12 | 1. [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) 13 | 2. [Node LTS](https://nodejs.org/en/) 14 | 3. Newest [Git](https://git-scm.com/downloads) 15 | 4. A code editor - we like [Visual Studio Code](https://code.visualstudio.com/download) 16 | - Also install [recommended extensions](https://code.visualstudio.com/docs/editor/extension-marketplace#_workspace-recommended-extensions) (e.g. [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)) 17 | 5. [Podman](https://podman.io/) or another container tool such as Docker Desktop 18 | 19 | #### Running Platform Receipt Locally 20 | 21 | The platform receipt component need to be run in **Docker**. 22 | 23 | ### Cloning the application 24 | Clone [Altinn Receipt repo](https://github.com/Altinn/altinn-receipt) and navigate to the folder. 25 | 26 | ```bash 27 | git clone https://github.com/Altinn/altinn-receipt 28 | cd altinn-receipt 29 | ``` 30 | __Prerequisite__ 31 | 1. This **Receipt** needs `app-localtest` for backend services. Before starting the `app-localtest` some modification would be needed in the docker-compose.yml file to set a couple of environment variables. 32 | 2. Also an app from **Altinn Studio** is needed for creating data that should be presented in the **Receipt**. 33 | 34 | 35 | __Process__ 36 | 37 | 1. **`app-localtest`**: Before starting `app-localtest` add these below lines to the `environment` section of `altinn_localtest` in the `docker-compose.yml` file of the **`app-localtest`**: 38 | ``` 39 | - PlatformSettings__ApiAuthorizationEndpoint=http://host.docker.internal:5101/authorization/api/v1/ 40 | - AuthnGeneralSettings__PlatformEndpoint=http://host.docker.internal:5101/ 41 | ``` 42 | After adding these the section `altinn_localtest` in the `docker-compose.yml` file of the **`app-localtest`** will look like this: 43 | ``` 44 | altinn_localtest: 45 | container_name: localtest 46 | image: localtest:latest 47 | restart: always 48 | networks: 49 | - altinntestlocal_network 50 | ports: 51 | - "5101:5101" 52 | build: 53 | context: . 54 | environment: 55 | - DOTNET_ENVIRONMENT=Docker 56 | - ASPNETCORE_URLS=http://*:5101/ 57 | - GeneralSettings__BaseUrl=http://${TEST_DOMAIN:-local.altinn.cloud}:${ALTINN3LOCAL_PORT:-80} 58 | - GeneralSettings__HostName=${TEST_DOMAIN:-local.altinn.cloud} 59 | - PlatformSettings__ApiAuthorizationEndpoint=http://host.docker.internal:5101/authorization/api/v1/ 60 | - AuthnGeneralSettings__PlatformEndpoint=http://host.docker.internal:5101/ 61 | volumes: 62 | - ./testdata:/testdata 63 | - ${ALTINN3LOCALSTORAGE_PATH:-AltinnPlatformLocal}:/AltinnPlatformLocal 64 | extra_hosts: 65 | - "host.docker.internal:host-gateway" 66 | ``` 67 | 68 | 2. Start the app you have made in the **Altinn Studio** and run it. Check if this app is working fine with the `app-localtest` backend service. 69 | 3. Then go to the altinn-receipt directory and run `podman compose up -d --build`. If you make changes to the code, you will need to re-run `podman compose up -d --build` to see the change in action. 70 | 4. The application should now be available at `local.altinn.cloud/receipt/{instanceOwnerId}/{instanceId}`. You'll find the `{instanceOwnerId}` and `{instanceId}` in the URL after you successfully submitted the **Altinn Studio** app form. 71 | -------------------------------------------------------------------------------- /src/frontend/src/utils/attachmentsUtils.ts: -------------------------------------------------------------------------------- 1 | import { IAttachment, IData, ITextResource, IAttachmentGrouping, IDataType, IApplication } from '../types/index'; 2 | import { getTextResourceByKey } from './language'; 3 | 4 | export function filterAppData(appData: IData[], dataTypes: IDataType[]) { 5 | const dataTypeIdsToExclude = dataTypes 6 | .filter((dataType) => { 7 | if (dataType.appLogic) { 8 | return true; 9 | } 10 | 11 | if (dataType.allowedContributers?.includes('app:owned') || dataType.allowedContributors?.includes('app:owned')) { 12 | return true; 13 | } 14 | 15 | return dataType.id === 'ref-data-as-pdf'; 16 | }) 17 | .map((dataType) => dataType.id); 18 | 19 | return appData.filter((it) => !dataTypeIdsToExclude.includes(it.dataType)); 20 | } 21 | 22 | export const mapAppDataToAttachments = (data: IData[], platform?: boolean): IAttachment[] => { 23 | if (!data) { 24 | return []; 25 | } 26 | 27 | return data.map((dataElement: IData) => { 28 | return { 29 | name: dataElement.filename, 30 | url: platform ? dataElement.selfLinks.platform : dataElement.selfLinks.apps, 31 | iconClass: 'reg reg-attachment', 32 | dataType: dataElement.dataType, 33 | }; 34 | }); 35 | }; 36 | 37 | export const getInstancePdf = (data: IData[], platform?: boolean): IAttachment[] => { 38 | if (!data) { 39 | return null; 40 | } 41 | 42 | const pdfElements = data.filter((element) => element.dataType === 'ref-data-as-pdf'); 43 | 44 | if (!pdfElements) { 45 | return null; 46 | } 47 | 48 | const result = pdfElements.map((element) => { 49 | const pdfUrl = platform ? element.selfLinks.platform : element.selfLinks.apps; 50 | return { 51 | name: element.filename, 52 | url: pdfUrl, 53 | iconClass: 'reg reg-attachment', 54 | dataType: element.dataType, 55 | }; 56 | }); 57 | return result; 58 | }; 59 | 60 | /** 61 | * Gets the attachment groupings from a list of attachments. 62 | * @param attachments the attachments 63 | * @param applicationMetadata the application metadata 64 | * @param textResources the application text resources 65 | */ 66 | export const getAttachmentGroupings = ( 67 | attachments: IAttachment[], 68 | applicationMetadata: IApplication, 69 | textResources: ITextResource[], 70 | ): IAttachmentGrouping => { 71 | const attachmentGroupings: IAttachmentGrouping = {}; 72 | 73 | if (!attachments || !applicationMetadata || !textResources) { 74 | return attachmentGroupings; 75 | } 76 | 77 | attachments.forEach((attachment: IAttachment) => { 78 | const grouping = getGroupingForAttachment(attachment, applicationMetadata); 79 | if ( 80 | grouping == null || 81 | applicationMetadata.attachmentGroupsToHide == null || 82 | !applicationMetadata.attachmentGroupsToHide.includes(grouping) 83 | ) { 84 | const title = getTextResourceByKey(grouping, textResources); 85 | if (!attachmentGroupings[title]) { 86 | attachmentGroupings[title] = []; 87 | } 88 | attachmentGroupings[title].push(attachment); 89 | } 90 | }); 91 | 92 | return attachmentGroupings; 93 | }; 94 | 95 | /** 96 | * Gets the grouping for a specific attachment 97 | * @param attachment the attachment 98 | * @param applicationMetadata the application metadata 99 | */ 100 | const getGroupingForAttachment = (attachment: IAttachment, applicationMetadata: IApplication): string => { 101 | if (!applicationMetadata || !applicationMetadata.dataTypes || !attachment) { 102 | return null; 103 | } 104 | 105 | const attachmentType = applicationMetadata.dataTypes.find( 106 | (dataType: IDataType) => dataType.id === attachment.dataType, 107 | ); 108 | 109 | if (!attachmentType || !attachmentType.grouping) { 110 | return null; 111 | } 112 | 113 | return attachmentType.grouping; 114 | }; 115 | -------------------------------------------------------------------------------- /.github/workflows/build-and-push-app.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [main] 5 | paths: 6 | - '.deploy/**' 7 | - '.github/workflows/build-and-push-app.yml' 8 | - 'src/**' 9 | - 'Dockerfile' 10 | 11 | permissions: 12 | id-token: write 13 | contents: read 14 | 15 | env: 16 | REGISTRY: ${{ secrets.ALTINN_REGISTRY }} 17 | APP_NAME: altinn-receipt 18 | 19 | jobs: 20 | docker-build-push: 21 | name: Build and push Docker image 22 | runs-on: ubuntu-latest 23 | outputs: 24 | short_sha: ${{ steps.set_short_sha.outputs.short_sha }} 25 | steps: 26 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 27 | 28 | - name: 'Azure login' 29 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 30 | with: 31 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 32 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 33 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 34 | 35 | - name: Set short SHA 36 | id: set_short_sha 37 | run: | 38 | SHORT_SHA=$(git rev-parse --short HEAD) 39 | echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_ENV 40 | echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT 41 | 42 | - name: Log in to Container registry 43 | run: | 44 | az acr login --name ${{ env.REGISTRY }} --expose-token --output tsv --query accessToken --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} --only-show-errors | docker login ${{ env.REGISTRY }} --username 00000000-0000-0000-0000-000000000000 --password-stdin 45 | - name: Docker build 46 | run: | 47 | docker build . -t ${{ env.REGISTRY }}/${{ env.APP_NAME }}:${{ env.SHORT_SHA }} 48 | - name: Docker push 49 | run: | 50 | docker push ${{ env.REGISTRY }}/${{ env.APP_NAME }}:${{ env.SHORT_SHA }} 51 | 52 | app-config-artifact-push: 53 | name: App config push 54 | runs-on: ubuntu-latest 55 | needs: docker-build-push 56 | strategy: 57 | matrix: 58 | environment: [at22, at23, at24, yt01, tt02, prod] 59 | environment: ${{ matrix.environment }} 60 | env: 61 | SHORT_SHA: ${{ needs.docker-build-push.outputs.short_sha }} 62 | steps: 63 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 64 | 65 | - name: 'Azure login' 66 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 67 | with: 68 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 69 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 70 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 71 | 72 | - name: Log in to Container registry 73 | run: | 74 | az acr login --name ${{ env.REGISTRY }} --expose-token --output tsv --query accessToken --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID }} --only-show-errors | docker login ${{ env.REGISTRY }} --username 00000000-0000-0000-0000-000000000000 --password-stdin 75 | 76 | - name: Setup Flux CLI 77 | uses: fluxcd/flux2/action@b6e76ca2534f76dcb8dd94fb057cdfa923c3b641 # v2.7.3 78 | 79 | - name: Replace TAG in app.yaml 80 | run: sed -i 's/\$SHA/${{ env.SHORT_SHA }}/g' .deploy/app.yaml 81 | 82 | - name: Push config artifact 83 | run: | 84 | flux push artifact oci://${{ env.REGISTRY }}/configs/${{ env.APP_NAME }}-${{ matrix.environment }}:${{ env.SHORT_SHA }} \ 85 | --path="./.deploy" \ 86 | --source="$(git config --get remote.origin.url)" \ 87 | --revision="$(git branch --show-current)/$(git rev-parse HEAD)" \ 88 | --annotations "org.opencontainers.image.description=altinn-receipt" \ 89 | --annotations "org.opencontainers.image.authors=team-core" 90 | 91 | - name: Tag artifact as latest 92 | run: | 93 | flux tag artifact oci://${{ env.REGISTRY }}/configs/${{ env.APP_NAME }}-${{ matrix.environment }}:${{ env.SHORT_SHA }} --tag latest 94 | -------------------------------------------------------------------------------- /src/frontend/src/components/atoms/AltinnAttachment.tsx: -------------------------------------------------------------------------------- 1 | import List from '@material-ui/core/List'; 2 | import ListItem from '@material-ui/core/ListItem'; 3 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 4 | import ListItemText from '@material-ui/core/ListItemText'; 5 | import createStyles from '@material-ui/core/styles/createStyles'; 6 | import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles'; 7 | import Typography from '@material-ui/core/Typography'; 8 | import classNames from 'classnames'; 9 | import React from 'react'; 10 | import { makeUrlRelativeIfSameDomain } from 'src/utils/urlHelper'; 11 | import { IAttachment } from 'src/types'; 12 | import { AltinnIcon } from 'src/components/AltinnIcon'; 13 | 14 | const styles = createStyles({ 15 | a: { 16 | '&:hover': { 17 | borderBottom: '0px', 18 | }, 19 | '&:focus': { 20 | borderBottom: '0px', 21 | }, 22 | '&:active': { 23 | borderBottom: '0px', 24 | }, 25 | '&:after': { 26 | display: 'none !important', 27 | }, 28 | }, 29 | listItemPadding: { 30 | paddingLeft: '2.0rem', 31 | }, 32 | listItemPaddingNone: { 33 | paddingLeft: '0rem', 34 | }, 35 | listItemTextPadding: { 36 | paddingLeft: '0', 37 | }, 38 | inline: { 39 | display: 'inline', 40 | }, 41 | primaryText: { 42 | fontWeight: 600, 43 | }, 44 | }); 45 | 46 | interface IAltinnAttachmentProps extends WithStyles { 47 | /** Attachments array with objects. See code example. */ 48 | attachments?: IAttachment[]; 49 | /** Disables vertical padding (does not currently work in Styleguidist) */ 50 | listDisableVerticalPadding?: boolean; 51 | /** Adds 2rem paddingLeft */ 52 | nested?: boolean; 53 | id?: string; 54 | } 55 | 56 | function ListItemLink(props: any) { 57 | return ( 58 | 63 | ); 64 | } 65 | 66 | 67 | export function AltinnAttachment(props: IAltinnAttachmentProps) { 68 | return( 69 | <> 70 | 71 | {props.attachments && props.attachments.map((attachment, index) => ( 72 | 83 | 84 | 89 | 90 | 93 | 100 | {attachment.name} 101 | 102 | 106 |  (last ned) 107 | 108 | 109 | } 110 | classes={{ 111 | root: classNames(props.classes.listItemTextPadding), 112 | }} 113 | /> 114 | 115 | ))} 116 | 117 | 118 | ); 119 | 120 | } 121 | 122 | export default withStyles(styles)(AltinnAttachment); 123 | -------------------------------------------------------------------------------- /src/frontend/src/utils/urlHelper.ts: -------------------------------------------------------------------------------- 1 | import type { IAltinnWindow } from '../types'; 2 | import { getReturnUrl } from './instance'; 3 | 4 | const { org, app } = window as Window as IAltinnWindow; 5 | const origin = window.location.origin; 6 | 7 | export const getApplicationMetadataUrl = (): string => { 8 | return `${origin}/designer/api/v1/${org}/${app}`; 9 | }; 10 | 11 | const prodStagingRegex = /^(?:\w+\.apps|platform)\.((\w+\.)?altinn\.(no|cloud))$/; 12 | const localRegex = /^local\.altinn\.cloud(:\d+)?$/; 13 | const localhostRegex = /^localhost(:\d+)?$/; 14 | 15 | function isLocalEnvironment(host: string): boolean { 16 | return localRegex.test(host) || localhostRegex.test(host); 17 | } 18 | 19 | function extractAltinnHost(host: string): string | undefined { 20 | const match = host.match(prodStagingRegex); 21 | return match?.[1]; 22 | } 23 | 24 | function isProductionEnvironment(altinnHost: string): boolean { 25 | return altinnHost === 'altinn.no'; 26 | } 27 | 28 | function buildArbeidsflateUrl(altinnHost: string): string { 29 | if (isProductionEnvironment(altinnHost)) { 30 | return 'https://af.altinn.no/'; 31 | } 32 | return `https://af.${altinnHost}/`; 33 | } 34 | 35 | export const returnBaseUrlToAltinn = (host: string): string | undefined => { 36 | const altinnHost = extractAltinnHost(host); 37 | if (!altinnHost) { 38 | return undefined; 39 | } 40 | return `https://${altinnHost}/`; 41 | }; 42 | 43 | function buildArbeidsflateRedirectUrl(host: string, partyId?: number, dialogId?: string): string | undefined { 44 | if (isLocalEnvironment(host)) { 45 | return `http://${host}/`; 46 | } 47 | 48 | const baseUrl = returnBaseUrlToAltinn(host); 49 | const altinnHost = extractAltinnHost(host); 50 | if (!baseUrl || !altinnHost) { 51 | return undefined; 52 | } 53 | 54 | const arbeidsflateBaseUrl = buildArbeidsflateUrl(altinnHost); 55 | const targetUrl = dialogId 56 | ? `${arbeidsflateBaseUrl.replace(/\/$/, '')}/inbox/${dialogId}` 57 | : arbeidsflateBaseUrl; 58 | 59 | if (partyId === undefined) { 60 | return targetUrl; 61 | } 62 | 63 | // Use A2 redirect mechanism with A3 arbeidsflate URL to maintain party context 64 | return `${baseUrl}ui/Reportee/ChangeReporteeAndRedirect?goTo=${encodeURIComponent(targetUrl)}&R=${partyId}`; 65 | } 66 | 67 | export function getDialogIdFromDataValues(dataValues: unknown): string | undefined { 68 | const data = dataValues as Record | null | undefined; 69 | const id = data?.['dialog.id']; 70 | if (typeof id === 'string') { 71 | return id; 72 | } 73 | if (typeof id === 'number') { 74 | return String(id); 75 | } 76 | return undefined; 77 | } 78 | 79 | export const returnUrlToMessagebox = (host: string, partyId?: number, dialogId?: string): string | undefined => { 80 | const customReturnUrl = getReturnUrl(); 81 | if (customReturnUrl) { 82 | return customReturnUrl; 83 | } 84 | 85 | return buildArbeidsflateRedirectUrl(host, partyId, dialogId); 86 | }; 87 | 88 | export const logoutUrlAltinn = (host: string): string | undefined => { 89 | if (isLocalEnvironment(host)) { 90 | return `http://${host}/`; 91 | } 92 | 93 | const baseUrl = returnBaseUrlToAltinn(host); 94 | if (!baseUrl) { 95 | return undefined; 96 | } 97 | return `${baseUrl}ui/authentication/LogOut`; 98 | }; 99 | 100 | // Storage is always returning https:// links for attachments. 101 | // on localhost (without https) this is a problem, so we make links 102 | // to the same domain as window.location.host relative. 103 | // "https://domain.com/a/b" => "/a/b" 104 | export const makeUrlRelativeIfSameDomain = ( 105 | url: string, 106 | location: Location = window.location, 107 | ) => { 108 | try { 109 | const parsed = new URL(url); 110 | if (parsed.hostname === location.hostname) { 111 | return parsed.pathname + parsed.search + parsed.hash; 112 | } 113 | } catch (_e) { 114 | //ignore invalid (or dummy) urls 115 | } 116 | return url; 117 | }; 118 | -------------------------------------------------------------------------------- /src/frontend/src/components/organisms/AltinnReceipt.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render as rtlRender, screen } from '@testing-library/react'; 3 | 4 | import AltinnReceipt from './AltinnReceipt'; 5 | 6 | const render = (props = {}) => { 7 | const allProps = { 8 | body: 'body', 9 | collapsibleTitle: 'collapsibleTitle', 10 | instanceMetaDataObject: {}, 11 | title: 'title', 12 | titleSubmitted: 'titleSubmitted', 13 | ...props, 14 | }; 15 | 16 | rtlRender(); 17 | }; 18 | 19 | const attachment1 = { 20 | name: 'attachment1Name', 21 | iconClass: 'attachment1IconClass', 22 | url: 'attachment1Url', 23 | dataType: 'attachment1DataType', 24 | }; 25 | const attachment2 = { 26 | name: 'attachment2Name', 27 | iconClass: 'attachment2IconClass', 28 | url: 'attachment2Url', 29 | dataType: 'attachment2DataType', 30 | }; 31 | 32 | describe('AltinnReceipt', () => { 33 | it('should not show titleSubmitted when there are no pdfs', () => { 34 | render(); 35 | 36 | expect( 37 | screen.queryByRole('heading', { 38 | name: /titlesubmitted/i, 39 | }), 40 | ).not.toBeInTheDocument(); 41 | 42 | expect(screen.queryByTestId('attachment-list')).not.toBeInTheDocument(); 43 | }); 44 | 45 | it('should show titleSubmitted when there are pdfs', () => { 46 | render({ pdf: [{}] }); 47 | 48 | expect( 49 | screen.getByRole('heading', { 50 | name: /titlesubmitted/i, 51 | }), 52 | ).toBeInTheDocument(); 53 | 54 | expect(screen.getByTestId('attachment-list')).toBeInTheDocument(); 55 | }); 56 | 57 | it('should show subtitle when set', () => { 58 | render({ subtitle: 'subtitle' }); 59 | 60 | expect(screen.getByText(/subtitle/i)).toBeInTheDocument(); 61 | }); 62 | 63 | it('should not show subtitle when not set', () => { 64 | render(); 65 | 66 | expect(screen.queryByText(/subtitle/i)).not.toBeInTheDocument(); 67 | }); 68 | 69 | it('should show body when set', () => { 70 | render({ body: 'body-text' }); 71 | 72 | expect(screen.getByText(/body-text/i)).toBeInTheDocument(); 73 | }); 74 | 75 | it('should not show body when not set', () => { 76 | render(); 77 | 78 | expect(screen.queryByText(/body-text/i)).not.toBeInTheDocument(); 79 | }); 80 | 81 | it('should show 2 attachments in default group when group name is not set', () => { 82 | render({ 83 | attachmentGroupings: { 84 | null: [attachment1, attachment2], 85 | }, 86 | collapsibleTitle: 'collapsibleTitle', 87 | hideCollapsibleCount: false, 88 | }); 89 | 90 | expect(screen.getByText(/collapsibletitle \(2\)/i)).toBeInTheDocument(); 91 | expect(screen.getByText(/attachment1name/i)).toBeInTheDocument(); 92 | expect(screen.getByText(/attachment2name/i)).toBeInTheDocument(); 93 | }); 94 | 95 | it('should not show collapsible count when hideCollapsibleCount is true', () => { 96 | render({ 97 | attachmentGroupings: { 98 | null: [attachment1, attachment2], 99 | }, 100 | collapsibleTitle: 'collapsibleTitle', 101 | hideCollapsibleCount: true, 102 | }); 103 | 104 | expect(screen.getByText(/collapsibletitle/i)).toBeInTheDocument(); 105 | expect( 106 | screen.queryByText(/collapsibletitle \(2\)/i), 107 | ).not.toBeInTheDocument(); 108 | }); 109 | 110 | it('should show attachments in defined groups', () => { 111 | render({ 112 | attachmentGroupings: { 113 | group1: [attachment1], 114 | group2: [attachment2], 115 | }, 116 | collapsibleTitle: 'collapsibleTitle', 117 | hideCollapsibleCount: false, 118 | }); 119 | 120 | expect(screen.getByText(/group1 \(1\)/i)).toBeInTheDocument(); 121 | expect(screen.getByText(/group2 \(1\)/i)).toBeInTheDocument(); 122 | expect(screen.getByText(/attachment1name/i)).toBeInTheDocument(); 123 | expect(screen.getByText(/attachment2name/i)).toBeInTheDocument(); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/frontend/src/components/organisms/AltinnAppHeader.tsx: -------------------------------------------------------------------------------- 1 | import { AppBar, Grid, Typography, makeStyles } from '@material-ui/core'; 2 | import React from 'react'; 3 | 4 | import type { IParty } from 'src/types'; 5 | import { renderPartyName } from 'src/utils/party'; 6 | import { AltinnLogo } from '../AltinnLogo'; 7 | import AltinnAppHeaderMenu from './AltinnAppHeaderMenu'; 8 | 9 | export interface IAltinnAppHeaderProps { 10 | /** The party of the instance owner */ 11 | party: IParty; 12 | /** The party of the currently logged in user */ 13 | userParty: IParty; 14 | /** The color used for the logos in the header */ 15 | logoColor: string; 16 | /** The header background color */ 17 | headerBackgroundColor: string; 18 | /** The logout text */ 19 | logoutText: string; 20 | /** The aria label text for profile menu */ 21 | ariaLabelIcon: string; 22 | } 23 | 24 | const useStyles = makeStyles(() => ({ 25 | altinnAppHeader: { 26 | boxShadow: 'none', 27 | WebkitBoxShadow: 'none', 28 | MozBoxShadow: 'none', 29 | }, 30 | mainContent: { 31 | width: '100%', 32 | marginLeft: 'auto', 33 | marginRight: 'auto', 34 | padding: 12, 35 | '@media (min-width:576px)': { 36 | maxWidth: 'none', 37 | padding: 24, 38 | }, 39 | '@media (min-width:760px)': { 40 | maxWidth: 'none', 41 | }, 42 | '@media (min-width:992px)': { 43 | maxWidth: 'none', 44 | }, 45 | '@media (min-width:1200px)': { 46 | maxWidth: 1056, 47 | paddingRight: 0, 48 | paddingLeft: 0, 49 | }, 50 | }, 51 | appHeaderText: { 52 | fontSize: 14, 53 | }, 54 | })); 55 | 56 | export function AltinnAppHeader({ 57 | logoColor, 58 | headerBackgroundColor, 59 | party, 60 | userParty, 61 | logoutText, 62 | ariaLabelIcon, 63 | }: IAltinnAppHeaderProps) { 64 | const classes = useStyles(); 65 | 66 | return ( 67 | 72 | 77 | 82 | 83 | 84 | 85 | 86 | 93 | 94 | {party && userParty && party.partyId === userParty.partyId && ( 95 | 96 | {renderPartyName(userParty)} 97 | 98 | )} 99 | {party && userParty && party.partyId !== userParty.partyId && ( 100 | 105 | 106 | 107 | {renderPartyName(userParty)} 108 | 109 | 110 | 111 | 112 | for {renderPartyName(party)} 113 | 114 | 115 | 116 | )} 117 | 118 | 119 | 125 | 126 | 127 | 128 | 129 | ); 130 | } 131 | 132 | export default AltinnAppHeader; 133 | -------------------------------------------------------------------------------- /src/frontend/src/utils/receiptUrlHelper.test.ts: -------------------------------------------------------------------------------- 1 | import { getInstanceId, getInstanceOwnerId } from './instance'; 2 | 3 | import { mockLocation } from 'testConfig/testUtils'; 4 | 5 | import { 6 | altinnAt22Url, 7 | altinnAt22PlatformUrl, 8 | getAltinnUrl, 9 | getTextResourceUrl, 10 | getUserUrl, 11 | getApplicationMetadataUrl, 12 | getAltinnCloudUrl, 13 | getExtendedInstanceUrl, 14 | } from './receiptUrlHelper'; 15 | 16 | const originalLocation = window.location; 17 | 18 | describe('receiptUrlHelper', () => { 19 | beforeEach(() => { 20 | mockLocation(originalLocation); 21 | }); 22 | 23 | describe('getAltinnUrl', () => { 24 | it('should return altinnAt22Url when hostname is localhost', () => { 25 | mockLocation({ hostname: 'localhost' }); 26 | 27 | expect(getAltinnUrl()).toEqual(altinnAt22Url); 28 | }); 29 | 30 | it('should return window.location.origin followed by trailing slash when hostname is not localhost', () => { 31 | mockLocation({ hostname: 'not-localhost', origin: 'https://origin' }); 32 | 33 | expect(getAltinnUrl()).toEqual('https://origin/'); 34 | }); 35 | }); 36 | 37 | describe('getTextResourceUrl', () => { 38 | it('should return correct path with args', () => { 39 | expect(getTextResourceUrl('org-name', 'app-name', 'nb')).toEqual( 40 | 'https://localhost/storage/api/v1/applications/org-name/app-name/texts/nb', 41 | ); 42 | }); 43 | }); 44 | 45 | describe('getUserUrl', () => { 46 | it('should return correct path with args', () => { 47 | expect(getUserUrl()).toEqual( 48 | 'https://localhost/receipt/api/v1/users/current', 49 | ); 50 | }); 51 | }); 52 | 53 | describe('getApplicationMetadataUrl', () => { 54 | it('should return correct path when running on localhost', () => { 55 | mockLocation({ hostname: 'localhost' }); 56 | 57 | expect(getApplicationMetadataUrl('org-name', 'app-name')).toEqual( 58 | `${altinnAt22PlatformUrl}storage/api/v1/applications/org-name/app-name`, 59 | ); 60 | }); 61 | 62 | it('should return correct path when not running on localhost', () => { 63 | mockLocation({ hostname: 'not-localhost', origin: 'https://origin' }); 64 | 65 | expect(getApplicationMetadataUrl('org-name', 'app-name')).toEqual( 66 | `https://origin/storage/api/v1/applications/org-name/app-name`, 67 | ); 68 | }); 69 | }); 70 | 71 | describe('getAltinnCloudUrl', () => { 72 | it('should return altinnAt22PlatformUrl when running on localhost', () => { 73 | mockLocation({ hostname: 'localhost' }); 74 | 75 | expect(getAltinnCloudUrl()).toEqual(altinnAt22PlatformUrl); 76 | }); 77 | 78 | it('should return altinnAt22PlatformUrl when running on 127.0.0.1', () => { 79 | mockLocation({ hostname: '127.0.0.1' }); 80 | 81 | expect(getAltinnCloudUrl()).toEqual(altinnAt22PlatformUrl); 82 | }); 83 | 84 | it('should return altinnAt22PlatformUrl when running on altinn3.no', () => { 85 | mockLocation({ hostname: 'altinn3.no' }); 86 | 87 | expect(getAltinnCloudUrl()).toEqual(altinnAt22PlatformUrl); 88 | }); 89 | 90 | it('should return window.location.origin followed by trailing slash when running on altinn3.no', () => { 91 | mockLocation({ hostname: 'not-localhost', origin: 'https://origin' }); 92 | 93 | expect(getAltinnCloudUrl()).toEqual('https://origin/'); 94 | }); 95 | }); 96 | 97 | describe('getExtendedInstanceUrl', () => { 98 | it('should return correct path when running on localhost', () => { 99 | mockLocation({ hostname: 'localhost' }); 100 | 101 | expect(getExtendedInstanceUrl()).toEqual( 102 | `${altinnAt22PlatformUrl}receipt/api/v1/instances/${getInstanceOwnerId()}/${getInstanceId()}?includeParty=true`, 103 | ); 104 | }); 105 | 106 | it('should return correct path when not running on localhost', () => { 107 | mockLocation({ hostname: 'not-localhost', origin: 'https://origin' }); 108 | 109 | expect(getExtendedInstanceUrl()).toEqual( 110 | `https://origin/receipt/api/v1/instances/${getInstanceOwnerId()}/${getInstanceId()}?includeParty=true`, 111 | ); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/frontend/src/utils/receipt.test.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | IAltinnOrg, 3 | IAltinnOrgs, 4 | IApplication, 5 | IData, 6 | IInstance, 7 | ILanguage, 8 | IParty, 9 | ITextResource, 10 | } from 'src/types/index'; 11 | 12 | import { getInstanceMetaDataObject } from './receipt'; 13 | 14 | describe('utils > receipt', () => { 15 | const instance = { 16 | data: [ 17 | { 18 | dataType: 'default', 19 | lastChanged: new Date(2018, 11, 24, 10, 33), 20 | } as IData, 21 | ], 22 | org: 'testOrg', 23 | } as IInstance; 24 | 25 | const partyPerson = { 26 | name: 'Ola Nordmann', 27 | ssn: '12345678', 28 | } as IParty; 29 | 30 | const partyOrg = { 31 | name: 'FIRMA AS', 32 | orgNumber: 12345, 33 | } as IParty; 34 | 35 | const language: ILanguage = { 36 | receipt_platform: { 37 | date_sent: 'Dato sendt', 38 | sender: 'Avsender', 39 | receiver: 'Mottaker', 40 | reference_number: 'Referansenummer', 41 | }, 42 | }; 43 | 44 | const organisations: IAltinnOrgs = { 45 | testOrg: { 46 | name: { 47 | nb: 'TEST ORG', 48 | }, 49 | } as unknown as IAltinnOrg, 50 | }; 51 | 52 | const expectedResult = { 53 | 'Dato sendt': '24.12.2018 / 10:33', 54 | Avsender: '12345678-Ola Nordmann', 55 | Mottaker: 'TEST ORG', 56 | Referansenummer: 'd6a414a797ae', 57 | }; 58 | 59 | const application = { 60 | dataTypes: [{ appLogic: true, id: 'default' }], 61 | } as IApplication; 62 | 63 | describe('getInstanceMetaDataObject', () => { 64 | it('should return instance metadata object with correct values for person', () => { 65 | const result = getInstanceMetaDataObject(instance, partyPerson, language, organisations, application, [], 'nb'); 66 | expect(result).toEqual(expectedResult); 67 | }); 68 | 69 | it('should return instance metadata object with correct values for org', () => { 70 | const result = getInstanceMetaDataObject(instance, partyOrg, language, organisations, application, [], 'nb'); 71 | const expectedOrgResult = expectedResult; 72 | expectedOrgResult.Avsender = '12345-FIRMA AS'; 73 | expect(result).toEqual(expectedOrgResult); 74 | }); 75 | 76 | it('should return empty object if no instance is provided', () => { 77 | const result = getInstanceMetaDataObject(undefined, partyOrg, language, organisations, application, [], 'nb'); 78 | 79 | expect(result).toEqual({}); 80 | }); 81 | 82 | it('should return empty object if no party is provided', () => { 83 | const result = getInstanceMetaDataObject(instance, undefined, language, organisations, application, [], 'nb'); 84 | 85 | expect(result).toEqual({}); 86 | }); 87 | 88 | it('should return empty object if no language is provided', () => { 89 | const result = getInstanceMetaDataObject(instance, partyOrg, undefined, organisations, application, [], 'nb'); 90 | 91 | expect(result).toEqual({}); 92 | }); 93 | 94 | it('should return empty object if no organisations is provided', () => { 95 | const result = getInstanceMetaDataObject(instance, partyOrg, language, undefined, application, [], 'nb'); 96 | 97 | expect(result).toEqual({}); 98 | }); 99 | 100 | it('should display appReceiver name from text resources if defined', () => { 101 | const textResourceWithAppOwner: ITextResource[] = [ 102 | { 103 | id: 'appReceiver', 104 | value: 'Name from resources', 105 | }, 106 | ]; 107 | const result = getInstanceMetaDataObject( 108 | instance, 109 | partyOrg, 110 | language, 111 | organisations, 112 | application, 113 | textResourceWithAppOwner, 114 | 'nb', 115 | ); 116 | expect(result.Mottaker).toEqual('Name from resources'); 117 | }); 118 | 119 | it('should return process.ended date if it exists', () => { 120 | const instanceWithEndedProcess = { 121 | ...instance, 122 | process: { 123 | ended: new Date(2019, 11, 24, 10, 33).toISOString(), 124 | }, 125 | } as IInstance; 126 | 127 | const result = getInstanceMetaDataObject( 128 | instanceWithEndedProcess, 129 | partyPerson, 130 | language, 131 | organisations, 132 | application, 133 | [], 134 | 'nb', 135 | ); 136 | expect(result['Dato sendt']).toEqual('24.12.2019 / 10:33'); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "receipt-react-app", 3 | "private": true, 4 | "scripts": { 5 | "start": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.development.js --mode development", 6 | "build": "cross-env NODE_ENV=production webpack --config webpack.config.production.js --progress", 7 | "test": "jest", 8 | "test:coverage": "jest --coverage --coverageReporters=lcov", 9 | "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint \"./src/**/*.ts*\"", 10 | "compile-ts": "tsc", 11 | "clean": "rimraf dist compiled" 12 | }, 13 | "author": "Altinn", 14 | "license": "3-Clause BSD", 15 | "dependencies": { 16 | "@babel/polyfill": "7.12.1", 17 | "@material-ui/core": "4.12.4", 18 | "axios": "1.12.2", 19 | "classnames": "2.5.1", 20 | "dompurify": "3.3.0", 21 | "html-react-parser": "5.2.7", 22 | "marked": "15.0.12", 23 | "moment": "2.30.1", 24 | "react": "18.3.1", 25 | "react-content-loader": "7.1.1", 26 | "react-dom": "18.3.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "7.28.5", 30 | "@babel/preset-env": "7.28.5", 31 | "@babel/preset-react": "7.28.5", 32 | "@testing-library/dom": "10.4.1", 33 | "@testing-library/jest-dom": "6.9.1", 34 | "@testing-library/react": "16.3.0", 35 | "@testing-library/user-event": "14.6.1", 36 | "@types/dompurify": "3.2.0", 37 | "@types/jest": "29.5.14", 38 | "@types/jsdom": "21.1.7", 39 | "@types/marked": "6.0.0", 40 | "@types/react": "18.3.26", 41 | "@types/react-dom": "18.3.7", 42 | "@typescript-eslint/eslint-plugin": "8.46.2", 43 | "@typescript-eslint/parser": "8.46.2", 44 | "altinn-designsystem": "4.2.0", 45 | "babel-jest": "29.7.0", 46 | "babel-loader": "10.0.0", 47 | "cross-env": "7.0.3", 48 | "css-loader": "7.1.2", 49 | "eslint": "9.38.0", 50 | "eslint-config-prettier": "10.1.8", 51 | "eslint-import-resolver-typescript": "4.4.4", 52 | "eslint-plugin-import": "2.32.0", 53 | "eslint-plugin-jsx-a11y": "6.10.2", 54 | "eslint-plugin-react": "7.37.5", 55 | "eslint-plugin-react-hooks": "5.2.0", 56 | "fork-ts-checker-notifier-webpack-plugin": "9.0.0", 57 | "fork-ts-checker-webpack-plugin": "9.1.0", 58 | "jest": "29.7.0", 59 | "jest-environment-jsdom": "29.7.0", 60 | "jest-fixed-jsdom": "^0.0.10", 61 | "jest-junit": "16.0.0", 62 | "jsdom": "26.1.0", 63 | "mini-css-extract-plugin": "2.9.4", 64 | "msw": "2.11.6", 65 | "prettier": "3.6.2", 66 | "react": "18.3.1", 67 | "react-dom": "18.3.1", 68 | "rimraf": "6.0.1", 69 | "source-map-loader": "5.0.0", 70 | "terser-webpack-plugin": "5.3.14", 71 | "ts-jest": "29.4.5", 72 | "ts-loader": "9.5.4", 73 | "typescript": "4.9.5", 74 | "undici": "^7.3.0", 75 | "webpack": "5.102.1", 76 | "webpack-cli": "6.0.1", 77 | "webpack-dev-server": "5.2.2" 78 | }, 79 | "packageManager": "yarn@3.8.7", 80 | "jest": { 81 | "transform": { 82 | "^.+\\.(ts|tsx)$": [ 83 | "ts-jest", 84 | { 85 | "isolatedModules": true 86 | } 87 | ], 88 | "^.+\\.(js|jsx|mjs|cjs)$": [ 89 | "babel-jest", 90 | { 91 | "presets": [ 92 | [ 93 | "@babel/preset-env", 94 | { 95 | "targets": { 96 | "node": "current" 97 | } 98 | } 99 | ] 100 | ] 101 | } 102 | ] 103 | }, 104 | "transformIgnorePatterns": [ 105 | "/node_modules/(?!(until-async|msw|@mswjs|@bundled-es-modules)/)" 106 | ], 107 | "moduleNameMapper": { 108 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 109 | "\\.(css|less)$": "/__mocks__/styleMock.js", 110 | "^src/(.*)$": "/src/$1", 111 | "^testConfig/(.*)$": "/testConfig/$1" 112 | }, 113 | "testRegex": "(/__tests__/.*|.*.(test|spec)).(ts|tsx|js|jsx)$", 114 | "moduleFileExtensions": [ 115 | "ts", 116 | "tsx", 117 | "js", 118 | "mjs" 119 | ], 120 | "setupFilesAfterEnv": [ 121 | "/testConfig/setupTests.ts" 122 | ], 123 | "testEnvironmentOptions": { 124 | "url": "https://localhost/receipt/mockInstanceOwnerId/6697de17-18c7-4fb9-a428-d6a414a797ae", 125 | "customExportConditions": [ 126 | "" 127 | ] 128 | }, 129 | "testEnvironment": "jest-fixed-jsdom" 130 | }, 131 | "browserslist": [ 132 | ">0.2%", 133 | "not dead", 134 | "not ie <= 10", 135 | "not op_mini all" 136 | ], 137 | "collectCoverageFrom": [ 138 | "!__tests__/**/*", 139 | "src/**/*.{ts,tsx}" 140 | ] 141 | } 142 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Extensions/HttpClientExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Altinn.Platform.Receipt.Extensions 6 | { 7 | /// 8 | /// This extentsion is created to make it easy to add a bearer token to a httprequests. 9 | /// 10 | public static class HttpClientExtension 11 | { 12 | /// 13 | /// Extension that add authorization header to request 14 | /// 15 | /// The httpclient 16 | /// the authorization token (jwt) 17 | /// The request Uri 18 | /// The http content 19 | /// The platformAccess tokens 20 | /// A HttpResponseMessage 21 | public static Task PostAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string platformAccessToken = null) 22 | { 23 | HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri); 24 | request.Headers.Add("Authorization", "Bearer " + authorizationToken); 25 | request.Content = content; 26 | if (!string.IsNullOrEmpty(platformAccessToken)) 27 | { 28 | request.Headers.Add("PlatformAccessToken", platformAccessToken); 29 | } 30 | 31 | return httpClient.SendAsync(request, CancellationToken.None); 32 | } 33 | 34 | /// 35 | /// Extension that add authorization header to request 36 | /// 37 | /// The httpclient 38 | /// the authorization token (jwt) 39 | /// The request Uri 40 | /// The http content 41 | /// The platformAccess tokens 42 | /// A HttpResponseMessage 43 | public static Task PutAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string platformAccessToken = null) 44 | { 45 | HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, requestUri); 46 | request.Headers.Add("Authorization", "Bearer " + authorizationToken); 47 | request.Content = content; 48 | if (!string.IsNullOrEmpty(platformAccessToken)) 49 | { 50 | request.Headers.Add("PlatformAccessToken", platformAccessToken); 51 | } 52 | 53 | return httpClient.SendAsync(request, CancellationToken.None); 54 | } 55 | 56 | /// 57 | /// Extension that add authorization header to request 58 | /// 59 | /// The httpclient 60 | /// the authorization token (jwt) 61 | /// The request Uri 62 | /// The platformAccess tokens 63 | /// A HttpResponseMessage 64 | public static Task GetAsync(this HttpClient httpClient, string authorizationToken, string requestUri, string platformAccessToken = null) 65 | { 66 | HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); 67 | request.Headers.Add("Authorization", "Bearer " + authorizationToken); 68 | if (!string.IsNullOrEmpty(platformAccessToken)) 69 | { 70 | request.Headers.Add("PlatformAccessToken", platformAccessToken); 71 | } 72 | 73 | return httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None); 74 | } 75 | 76 | /// 77 | /// Extension that add authorization header to request 78 | /// 79 | /// The httpclient 80 | /// the authorization token (jwt) 81 | /// The request Uri 82 | /// The platformAccess tokens 83 | /// A HttpResponseMessage 84 | public static Task DeleteAsync(this HttpClient httpClient, string authorizationToken, string requestUri, string platformAccessToken = null) 85 | { 86 | HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, requestUri); 87 | request.Headers.Add("Authorization", "Bearer " + authorizationToken); 88 | if (!string.IsNullOrEmpty(platformAccessToken)) 89 | { 90 | request.Headers.Add("PlatformAccessToken", platformAccessToken); 91 | } 92 | 93 | return httpClient.SendAsync(request, CancellationToken.None); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/frontend/src/features/receipt/Receipt.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render as rtlRender, screen, waitFor } from '@testing-library/react'; 3 | 4 | import Receipt from './Receipt'; 5 | import { setupServer, handlers, instanceHandler, textsHandler } from 'testConfig/testUtils'; 6 | import { instanceWithPdf, instanceWithSubstatus, texts } from 'testConfig/apiResponses'; 7 | 8 | const server = setupServer(...handlers); 9 | 10 | const render = () => { 11 | rtlRender(); 12 | }; 13 | 14 | describe('Receipt', () => { 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it('should show "Loading..." while data is loading, and should show "Kvittering" when data is loaded', async () => { 20 | render(); 21 | 22 | expect(screen.getByText('Loading...')).toBeInTheDocument(); 23 | 24 | await waitFor(() => { 25 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 26 | }); 27 | 28 | expect(screen.getByText('Kvittering')).toBeInTheDocument(); 29 | }); 30 | 31 | it('should show download link to pdf when all data is loaded, and data includes pdf', async () => { 32 | server.use(instanceHandler(instanceWithPdf)); 33 | 34 | render(); 35 | 36 | await waitFor(() => { 37 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 38 | }); 39 | 40 | await waitFor(() => { 41 | expect( 42 | screen.getByRole('link', { 43 | name: /ui komponents app\.pdf/i, 44 | }), 45 | ).toBeInTheDocument(); 46 | }); 47 | 48 | expect(screen.getAllByRole('link').length).toBe(1); 49 | }); 50 | 51 | it('should not show download link to pdf when all data is loaded, and data does not include pdf', async () => { 52 | render(); 53 | 54 | await waitFor(() => { 55 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 56 | }); 57 | 58 | expect(screen.queryAllByRole('link').length).toBe(0); 59 | }); 60 | 61 | it('should show substatus when instance data contains substatus information', async () => { 62 | server.use(instanceHandler(instanceWithSubstatus)); 63 | render(); 64 | 65 | await waitFor(() => { 66 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 67 | }); 68 | 69 | expect(screen.getByTestId('receipt-substatus')).toBeInTheDocument(); 70 | }); 71 | 72 | it('should not show substatus when instance data does not containe substatus information', async () => { 73 | render(); 74 | 75 | await waitFor(() => { 76 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 77 | }); 78 | 79 | expect(screen.queryByTestId('receipt-substatus')).not.toBeInTheDocument(); 80 | }); 81 | 82 | it('should show customised text when textResources contains overrides', async () => { 83 | const textsWithOverrides = { 84 | ...texts, 85 | resources: [ 86 | ...texts.resources, 87 | { 88 | id: 'receipt_platform.helper_text', 89 | value: 'Help text override', 90 | }, 91 | ], 92 | }; 93 | server.use(textsHandler(textsWithOverrides)); 94 | 95 | render(); 96 | 97 | await waitFor(() => { 98 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 99 | expect(screen.getByText('Help text override')).toBeInTheDocument(); 100 | }); 101 | }); 102 | 103 | it('should show customised text with variables when textResources contains overrides', async () => { 104 | const textsWithOverrides = { 105 | ...texts, 106 | resources: [ 107 | ...texts.resources, 108 | { 109 | id: 'receipt_platform.helper_text', 110 | value: 'Help text override with instanceOwnerPartyId variable: {0}', 111 | variables: [ 112 | { 113 | key: 'instanceOwnerPartyId', 114 | dataSource: 'instanceContext', 115 | }, 116 | ], 117 | }, 118 | ], 119 | }; 120 | server.use(textsHandler(textsWithOverrides)); 121 | 122 | render(); 123 | 124 | await waitFor(() => { 125 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 126 | expect(screen.getByText('Help text override with instanceOwnerPartyId variable: 512345')).toBeInTheDocument(); 127 | }); 128 | }); 129 | 130 | it('should parse customised text with markdown when textResources contains overrides', async () => { 131 | const textsWithOverrides = { 132 | ...texts, 133 | resources: [ 134 | ...texts.resources, 135 | { 136 | id: 'receipt_platform.helper_text', 137 | value: `Help text with [a link to altinn](https://altinn.no)`, 138 | }, 139 | ], 140 | }; 141 | server.use(textsHandler(textsWithOverrides)); 142 | 143 | render(); 144 | 145 | await waitFor(() => { 146 | expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); 147 | expect( 148 | screen.getByRole('link', { 149 | name: /a link to altinn/i, 150 | }), 151 | ).toBeInTheDocument(); 152 | }); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/frontend/src/components/molecules/AltinnModal.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme, createStyles, IconButton, Modal, Typography } from '@material-ui/core'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import classNames from 'classnames'; 4 | import React from 'react'; 5 | import altinnTheme from 'src/theme/altinnStudioTheme'; 6 | 7 | export interface IAltinnModalComponentProvidedProps { 8 | /** @ignore */ 9 | classes: any; 10 | /** Text or react element shown in the header */ 11 | headerText?: any; 12 | /** Boolean value of the modal being open or not */ 13 | isOpen: boolean; 14 | /** Show close-icon outside modal */ 15 | closeButtonOutsideModal?: boolean; 16 | /** Callback function for when the modal is closed */ 17 | onClose: any; 18 | /** Boolean value for hiding the background shower */ 19 | hideBackdrop?: boolean; 20 | /** Boolean value for hiding the X button in the header */ 21 | hideCloseIcon?: boolean; 22 | /** Boolean value for allowing modal to close on backdrop click */ 23 | allowCloseOnBackdropClick?: boolean; 24 | /** Boolean value for showing print view */ 25 | printView?: boolean; 26 | children?: React.ReactNode; 27 | } 28 | 29 | export interface IAltinnModalComponentState { 30 | isOpen: boolean; 31 | } 32 | 33 | const theme = createTheme(altinnTheme); 34 | 35 | const styles = createStyles({ 36 | modal: { 37 | [theme.breakpoints.down('sm')]: { 38 | width: '95%', 39 | }, 40 | [theme.breakpoints.up('md')]: { 41 | width: '80%', 42 | }, 43 | maxWidth: '875px', 44 | backgroundColor: theme.altinnPalette.primary.white, 45 | boxShadow: theme.shadows[5], 46 | outline: 'none', 47 | marginRight: 'auto', 48 | marginLeft: 'auto', 49 | marginTop: '9.68rem', 50 | marginBottom: '10%', 51 | ['@media only print']: { 52 | boxShadow: '0 0 0 0 !important', 53 | }, 54 | }, 55 | header: { 56 | backgroundColor: altinnTheme.altinnPalette.primary.blueDarker, 57 | paddingLeft: 12, 58 | '@media (min-width: 786px)': { 59 | paddingLeft: 96, 60 | }, 61 | paddingTop: 30, 62 | paddingBottom: 30, 63 | }, 64 | headerText: { 65 | fontSize: '2.8rem', 66 | color: altinnTheme.altinnPalette.primary.white, 67 | }, 68 | body: { 69 | paddingLeft: 12, 70 | paddingRight: 12, 71 | paddingTop: 24, 72 | paddingBottom: 34, 73 | ['@media only print']: { 74 | paddingLeft: 48, 75 | }, 76 | '@media (min-width: 786px)': { 77 | paddingLeft: 96, 78 | paddingRight: 96, 79 | paddingTop: 34, 80 | }, 81 | }, 82 | iconBtn: { 83 | float: 'right', 84 | marginRight: '-11px', 85 | marginTop: '-27px', 86 | }, 87 | iconStyling: { 88 | color: altinnTheme.altinnPalette.primary.white, 89 | fontSize: 38, 90 | }, 91 | closeButtonOutsideModal: { 92 | position: 'relative', 93 | top: -60, 94 | }, 95 | scroll: { 96 | overflow: 'overlay', 97 | }, 98 | }); 99 | 100 | export class AltinnModal extends React.Component { 101 | public render() { 102 | const { classes, printView } = this.props; 103 | if (!printView) { 104 | return ( 105 | 111 |
112 |
113 | {this.props.hideCloseIcon && this.props.hideCloseIcon === true ? null : 114 | 121 | 122 | 123 | } 124 | 125 | {this.props.headerText} 126 | 127 |
128 |
129 | {this.props.children} 130 |
131 |
132 |
133 | ); 134 | } else { 135 | return ( 136 |
137 |
138 | {this.props.hideCloseIcon && this.props.hideCloseIcon === true ? null : 139 | 146 | 147 | 148 | } 149 | 150 | {this.props.headerText} 151 | 152 |
153 |
154 | {this.props.children} 155 |
156 |
157 | ); 158 | } 159 | } 160 | } 161 | 162 | export default withStyles(styles)(AltinnModal); 163 | -------------------------------------------------------------------------------- /src/frontend/src/components/organisms/AltinnReceipt.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@material-ui/core'; 2 | import { 3 | createTheme, 4 | createStyles, 5 | MuiThemeProvider, 6 | WithStyles, 7 | withStyles, 8 | } from '@material-ui/core/styles'; 9 | import React from 'react'; 10 | import useMediaQuery from '@material-ui/core/useMediaQuery'; 11 | import altinnTheme from '../../theme/altinnAppTheme'; 12 | import { IAttachment, IAttachmentGrouping } from '../../types'; 13 | import AltinnAttachmentComponent from 'src/components/atoms/AltinnAttachment'; 14 | import AltinnCollapsibleAttachmentsComponent from '../molecules/AltinnCollapsibleAttachments'; 15 | import AltinnSummaryTable from '../molecules/AltinnSummaryTable'; 16 | 17 | export interface IReceiptComponentProps extends WithStyles { 18 | attachmentGroupings?: IAttachmentGrouping; 19 | body: React.ReactNode; 20 | collapsibleTitle: React.ReactNode; 21 | hideCollapsibleCount?: boolean; 22 | instanceMetaDataObject: any; 23 | pdf?: IAttachment[]; 24 | subtitle?: boolean; 25 | subtitleurl?: string; 26 | title: React.ReactNode; 27 | titleSubmitted: React.ReactNode; 28 | } 29 | 30 | const theme = createTheme(altinnTheme); 31 | 32 | const styles = createStyles({ 33 | instanceMetaData: { 34 | marginTop: 36, 35 | }, 36 | tableCell: { 37 | borderBottom: 0, 38 | paddingRight: '2.5rem', 39 | }, 40 | tableRow: { 41 | height: 'auto', 42 | }, 43 | paddingTop24: { 44 | paddingTop: '2.4rem', 45 | }, 46 | wordBreak: { 47 | wordBreak: 'break-word', 48 | }, 49 | }); 50 | 51 | interface ICollapsibleAttacments { 52 | attachments: IAttachment[]; 53 | title: React.ReactNode; 54 | hideCollapsibleCount?: boolean; 55 | } 56 | 57 | const CollapsibleAttachments = ({ 58 | attachments, 59 | title, 60 | hideCollapsibleCount, 61 | }: ICollapsibleAttacments) => { 62 | return ( 63 | 4) 67 | } 68 | title={title} 69 | hideCount={hideCollapsibleCount} 70 | /> 71 | ); 72 | }; 73 | 74 | interface IRenderAttachmentGroupings { 75 | attachmentGroupings?: IAttachmentGrouping; 76 | collapsibleTitle: React.ReactNode; 77 | hideCollapsibleCount?: boolean; 78 | } 79 | 80 | const RenderAttachmentGroupings = ({ 81 | attachmentGroupings, 82 | collapsibleTitle, 83 | hideCollapsibleCount, 84 | }: IRenderAttachmentGroupings) => { 85 | const groupings = attachmentGroupings; 86 | const groups: JSX.Element[] = []; 87 | 88 | if (!groupings) { 89 | return null; 90 | } 91 | 92 | if (groupings.null) { 93 | // we have attachments that does not have a grouping. Render them first with default title 94 | groups.push( 95 | , 100 | ); 101 | } 102 | 103 | Object.keys(groupings || {}).forEach((title: string) => { 104 | if (title && title !== 'null') { 105 | groups.push( 106 | , 111 | ); 112 | } 113 | }); 114 | 115 | return ( 116 | <> 117 | {groups.map((element: JSX.Element, index) => { 118 | return {element}; 119 | })} 120 | 121 | ); 122 | }; 123 | 124 | export function ReceiptComponent(props: IReceiptComponentProps) { 125 | // renders attachment groups. Always shows default group first 126 | return ( 127 |
128 | 129 | {props.title} 130 | 131 | {props.subtitle && ( 132 | 133 | {props.subtitle} 134 | 135 | )} 136 | 137 | 142 | {props.body} 143 | 144 | {props.pdf && props.pdf.length > 0 && ( 145 | <> 146 | {props.titleSubmitted && ( 147 | 155 | {props.titleSubmitted} 156 | 157 | )} 158 | 162 | 163 | ) 164 | } 165 | {props.attachmentGroupings && ( 166 | 171 | )} 172 | 173 |
174 | ); 175 | } 176 | 177 | export default withStyles(styles)(ReceiptComponent); 178 | -------------------------------------------------------------------------------- /src/backend/Altinn.Receipt/Telemetry/RequestFilterProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Frozen; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | using System.Text.Json; 8 | using Altinn.AccessManagement.Core.Models; 9 | using AltinnCore.Authentication.Constants; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.Primitives; 12 | using OpenTelemetry; 13 | 14 | namespace Altinn.Platform.Receipt.Telemetry 15 | { 16 | /// 17 | /// Filter for requests (and child dependencies) that should not be logged. 18 | /// 19 | public class RequestFilterProcessor : BaseProcessor 20 | { 21 | private const string RequestKind = "Microsoft.AspNetCore.Hosting.HttpRequestIn"; 22 | private readonly IHttpContextAccessor _httpContextAccessor; 23 | private static readonly FrozenDictionary> _claimActions = InitClaimActions(); 24 | 25 | private static FrozenDictionary> InitClaimActions() 26 | { 27 | var actions = new Dictionary>(StringComparer.OrdinalIgnoreCase) 28 | { 29 | { 30 | AltinnCoreClaimTypes.UserId, 31 | static (claim, activity) => 32 | { 33 | activity.SetTag("user.id", claim.Value); 34 | } 35 | }, 36 | { 37 | AltinnCoreClaimTypes.PartyID, 38 | static (claim, activity) => 39 | { 40 | activity.SetTag("user.party.id", claim.Value); 41 | } 42 | }, 43 | { 44 | AltinnCoreClaimTypes.AuthenticationLevel, 45 | static (claim, activity) => 46 | { 47 | activity.SetTag("user.authentication.level", claim.Value); 48 | } 49 | }, 50 | { 51 | AltinnCoreClaimTypes.Org, 52 | static (claim, activity) => 53 | { 54 | activity.SetTag("user.application.owner.id", claim.Value); 55 | } 56 | }, 57 | { 58 | AltinnCoreClaimTypes.OrgNumber, 59 | static (claim, activity) => 60 | { 61 | activity.SetTag("user.organization.number", claim.Value); 62 | } 63 | }, 64 | { 65 | "authorization_details", 66 | static (claim, activity) => 67 | { 68 | SystemUserClaim claimValue = JsonSerializer.Deserialize(claim.Value); 69 | activity.SetTag("user.system.id", claimValue?.Systemuser_id[0] ?? null); 70 | activity.SetTag("user.system.owner.number", claimValue?.Systemuser_org.ID ?? null); 71 | } 72 | }, 73 | }; 74 | 75 | return actions.ToFrozenDictionary(); 76 | } 77 | 78 | /// 79 | /// Initializes a new instance of the class. 80 | /// 81 | public RequestFilterProcessor(IHttpContextAccessor httpContextAccessor = null) : base() 82 | { 83 | _httpContextAccessor = httpContextAccessor; 84 | } 85 | 86 | /// 87 | /// Determine whether to skip a request 88 | /// 89 | public override void OnStart(Activity activity) 90 | { 91 | bool skip = false; 92 | if (activity.OperationName == RequestKind) 93 | { 94 | skip = ExcludeRequest(_httpContextAccessor.HttpContext.Request.Path.Value); 95 | } 96 | else if (!(activity.Parent?.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded) ?? true)) 97 | { 98 | skip = true; 99 | } 100 | 101 | if (skip) 102 | { 103 | activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; 104 | } 105 | } 106 | 107 | /// 108 | /// No action on end 109 | /// 110 | /// xx 111 | public override void OnEnd(Activity activity) 112 | { 113 | if (activity.OperationName == RequestKind && _httpContextAccessor.HttpContext is not null) 114 | { 115 | if (_httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-Forwarded-For", out StringValues ipAddress)) 116 | { 117 | activity.SetTag("ipAddress", ipAddress.FirstOrDefault()); 118 | } 119 | 120 | foreach (var claim in _httpContextAccessor.HttpContext.User.Claims) 121 | { 122 | if (_claimActions.TryGetValue(claim.Type, out var action)) 123 | { 124 | action(claim, activity); 125 | } 126 | } 127 | } 128 | } 129 | 130 | private bool ExcludeRequest(string localpath) 131 | { 132 | return localpath switch 133 | { 134 | var path when path.TrimEnd('/').EndsWith("/health", StringComparison.OrdinalIgnoreCase) => true, 135 | _ => false 136 | }; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/frontend/src/features/receipt/Receipt.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles, createTheme, Grid, WithStyles, withStyles } from '@material-ui/core'; 2 | import useMediaQuery from '@material-ui/core/useMediaQuery'; 3 | import React from 'react'; 4 | 5 | import type { IAttachment, IParty } from 'src/types'; 6 | 7 | import { AltinnAppHeader, AltinnContentLoader, AltinnModal, AltinnReceipt, AltinnSubstatusPaper } from 'src/components'; 8 | import AltinnReceiptTheme from 'src/theme/altinnReceiptTheme'; 9 | import { 10 | filterAppData, 11 | getAttachmentGroupings, 12 | getInstancePdf, 13 | mapAppDataToAttachments, 14 | } from 'src/utils/attachmentsUtils'; 15 | import { getAppName, getLanguageFromKey, getParsedLanguageFromKey, getTextResourceByKey } from 'src/utils/language'; 16 | import { getInstanceMetaDataObject } from 'src/utils/receipt'; 17 | import { returnUrlToMessagebox, getDialogIdFromDataValues } from 'src/utils/urlHelper'; 18 | 19 | import { useFetchInitialData, useLanguageWithOverrides } from './hooks'; 20 | 21 | const theme = createTheme(AltinnReceiptTheme); 22 | 23 | const styles = () => 24 | createStyles({ 25 | body: { 26 | '@media only print': { 27 | paddingLeft: '48px !important', 28 | }, 29 | }, 30 | substatus: { 31 | maxWidth: '875px', 32 | [theme.breakpoints.down('sm')]: { 33 | width: '95%', 34 | }, 35 | [theme.breakpoints.up('md')]: { 36 | width: '80%', 37 | }, 38 | }, 39 | }); 40 | 41 | function Receipt(props: WithStyles) { 42 | const [attachments, setAttachments] = React.useState(); 43 | const [pdf, setPdf] = React.useState(); 44 | 45 | const { application, textResources, party, instance, organisations, user } = useFetchInitialData(); 46 | 47 | const { language } = useLanguageWithOverrides({ 48 | textResources, 49 | instance, 50 | user, 51 | }); 52 | 53 | const isPrint = useMediaQuery('print'); 54 | 55 | const getTitle = (): React.ReactNode => { 56 | const applicationTitle = getAppName(textResources, application, user.profileSettingPreference.language); 57 | 58 | return ( 59 | <> 60 | {applicationTitle}{' '} 61 | {instance.isA2Lookup ? '' : getParsedLanguageFromKey('receipt_platform.is_sent', language)} 62 | 63 | ); 64 | }; 65 | 66 | const handleModalClose = () => { 67 | const partyId = instance?.instanceOwner?.partyId ? Number(instance.instanceOwner.partyId) : undefined; 68 | const dialogId = getDialogIdFromDataValues(instance?.dataValues); 69 | const returnUrl = returnUrlToMessagebox(window.location.host, partyId, dialogId); 70 | if (returnUrl) { 71 | window.location.href = returnUrl; 72 | } 73 | }; 74 | 75 | const isLoading = !party || !instance || !organisations || !application || !language || !user || !textResources; 76 | 77 | React.useEffect(() => { 78 | if (instance && application) { 79 | const filteredAppData = filterAppData(instance.data, application.dataTypes); 80 | const attachments = mapAppDataToAttachments(filteredAppData, true); 81 | 82 | setAttachments(attachments); 83 | setPdf(getInstancePdf(instance.data, true)); 84 | } 85 | }, [instance, application]); 86 | 87 | return ( 88 | 94 | 102 | {instance?.status?.substatus && ( 103 | 108 | 112 | 113 | )} 114 | 124 | {isLoading ? ( 125 | 126 | ) : ( 127 | 149 | )} 150 | 151 | 152 | ); 153 | } 154 | 155 | export default withStyles(styles)(Receipt); 156 | -------------------------------------------------------------------------------- /src/frontend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface IAltinnWindow extends Window { 2 | org: string; 3 | app: string; 4 | } 5 | 6 | export interface IApplication { 7 | createdBy: string; 8 | created: string; 9 | dataTypes: IDataType[]; 10 | id: string; 11 | lastChangedBy: string; 12 | lastChanged: string; 13 | org: string; 14 | partyTypesAllowed: IPartyTypesAllowed; 15 | title: ITitle; 16 | onEntry?: IOnEntry; 17 | attachmentGroupsToHide?: string[]; 18 | } 19 | 20 | export interface IAltinnOrg { 21 | name: ITitle; 22 | logo: string; 23 | orgnr: string; 24 | homepage: string; 25 | environments: string[]; 26 | } 27 | 28 | export interface IAltinnOrgs { 29 | [org: string]: IAltinnOrg; 30 | } 31 | 32 | export interface IOnEntry { 33 | show: 'new-instance' | 'startpage' | string; 34 | } 35 | 36 | export interface IAttachment { 37 | name: string; 38 | iconClass: string; 39 | url: string; 40 | dataType: string; 41 | tags?: string[]; 42 | } 43 | 44 | export interface IData { 45 | id: string; 46 | instanceGuid: string; 47 | dataType: string; 48 | filename?: string; 49 | contentType: string; 50 | blobStoragePath: string; 51 | selfLinks?: ISelfLinks; 52 | size: number; 53 | locked: boolean; 54 | refs: string[]; 55 | isRead?: boolean; 56 | tags?: string[]; 57 | created: Date; 58 | createdBy: string; 59 | lastChanged: Date; 60 | lastChangedBy: string; 61 | } 62 | 63 | export interface IDataType { 64 | id: string; 65 | description?: string; 66 | allowedContentTypes: string[]; 67 | /** 68 | * @deprecated Will be removed in future versions. 69 | */ 70 | allowedContributers?: string[]; 71 | allowedContributors?: string[]; 72 | appLogic?: any; 73 | taskId?: string; 74 | maxSize?: number; 75 | maxCount: number; 76 | minCount: number; 77 | grouping?: string; 78 | } 79 | 80 | export interface IExtendedInstance { 81 | instance: IInstance; 82 | party: IParty; 83 | } 84 | 85 | export interface IInstance { 86 | appId: string; 87 | created: Date; 88 | data: IData[]; 89 | dueBefore?: Date; 90 | id: string; 91 | instanceOwner: IInstanceOwner; 92 | instanceState: IInstanceState; 93 | lastChanged: Date; 94 | org: string; 95 | process: IProcess; 96 | selfLinks: ISelfLinks; 97 | status: IInstanceStatus; 98 | title: ITitle; 99 | visibleAfter?: Date; 100 | dataValues?: any; 101 | isA2Lookup?: boolean; 102 | } 103 | 104 | export interface IInstanceStatus { 105 | archived?: Date; 106 | isArchived: boolean; 107 | substatus: ISubstatus; 108 | } 109 | 110 | export interface ISubstatus { 111 | label: string; 112 | description: string; 113 | } 114 | 115 | export interface IInstanceOwner { 116 | partyId: string; 117 | personNumber?: string; 118 | organisationNumber?: string; 119 | } 120 | 121 | export interface IInstanceState { 122 | isDeleted: boolean; 123 | isMarkedForHardDelete: boolean; 124 | isArchived: boolean; 125 | } 126 | 127 | export interface ILanguage { 128 | [key: string]: string | ILanguage; 129 | } 130 | 131 | export interface IOrganisation { 132 | orgNumber: string; 133 | name: string; 134 | unitType: string; 135 | telephoneNumber: string; 136 | mobileNumber: string; 137 | faxNumber: string; 138 | emailAdress: string; 139 | internetAdress: string; 140 | mailingAdress: string; 141 | mailingPostalCode: string; 142 | mailingPostalCity: string; 143 | businessPostalCode: string; 144 | businessPostalCity: string; 145 | } 146 | 147 | export interface IParty { 148 | partyId: string; 149 | partyTypeName: number; 150 | orgNumber: number; 151 | ssn: string; 152 | unitType: string; 153 | name: string; 154 | isDeleted: boolean; 155 | onlyHierarchyElementWithNoAccess: boolean; 156 | person?: IPerson; 157 | organisation?: IOrganisation; 158 | childParties: IParty[]; 159 | } 160 | 161 | export interface IPartyTypesAllowed { 162 | bankruptcyEstate: boolean; 163 | organisation: boolean; 164 | person: boolean; 165 | subUnit: boolean; 166 | } 167 | 168 | export interface IPerson { 169 | ssn: string; 170 | name: string; 171 | firstName: string; 172 | middleName: string; 173 | lastName: string; 174 | telephoneNumber: string; 175 | mobileNumber: string; 176 | mailingAddress: string; 177 | mailingPostalCode: number; 178 | mailingPostalCity: string; 179 | addressMunicipalNumber: number; 180 | addressMunicipalName: string; 181 | addressStreetName: string; 182 | addressHouseNumber: number; 183 | addressHouseLetter: string; 184 | addressPostalCode: number; 185 | addressCity: string; 186 | } 187 | 188 | export interface IProcess { 189 | started: string; 190 | startEvent: string; 191 | currentTask: ITask; 192 | ended: string; 193 | endEvent: string; 194 | } 195 | 196 | export interface IProfile { 197 | userId: number; 198 | userName: string; 199 | phoneNumber?: any; 200 | email?: any; 201 | partyId: number; 202 | party: IParty; 203 | userType: number; 204 | profileSettingPreference: IProfileSettingPreference; 205 | } 206 | 207 | export interface IProfileSettingPreference { 208 | language: string; 209 | preSelectedPartyId: number; 210 | doNotPromptForParty: boolean; 211 | } 212 | 213 | export interface IUserCookieLanguage { 214 | language: string; 215 | } 216 | 217 | export interface IAttachmentGroupsToHide { 218 | attachmentgroupstohide: string; 219 | } 220 | 221 | export interface ISelfLinks { 222 | apps: string; 223 | platform: string; 224 | } 225 | 226 | export interface ITask { 227 | flow: number; 228 | started: string; 229 | elementId: string; 230 | name: string; 231 | altinnTaskType: string; 232 | ended: string; 233 | validated: IValidated; 234 | } 235 | 236 | export interface ITitle { 237 | [key: string]: string; 238 | } 239 | 240 | export interface IValidated { 241 | timestamp: string; 242 | canCompleteTask: boolean; 243 | } 244 | 245 | export interface ITextResource { 246 | id: string; 247 | value: string; 248 | unparsedValue?: string; 249 | variables?: IVariable[]; 250 | repeating?: boolean; 251 | } 252 | 253 | export interface IVariable { 254 | key: string; 255 | dataSource: string; 256 | } 257 | 258 | export interface IAttachmentGrouping { 259 | [title: string]: IAttachment[]; 260 | } 261 | 262 | export interface IDataSource { 263 | [key: string]: any; 264 | } 265 | 266 | export interface IDataSources { 267 | [key: string]: IDataSource; 268 | } 269 | 270 | export interface IApplicationSettings { 271 | [source: string]: string; 272 | } 273 | 274 | /** Describes an object with key values from current instance to be used in texts. */ 275 | export interface IInstanceContext { 276 | instanceId: string; 277 | appId: string; 278 | instanceOwnerPartyId: string; 279 | } 280 | --------------------------------------------------------------------------------