Skip to content

Commit

Permalink
Support image-based R&C for Paper LPAs
Browse files Browse the repository at this point in the history
- Alter schema to change fields available based on form channel
- Add `FileUpload` and `File` struct types
  - `FileUpload` is used in submissions, `File` is used to pass back
- Add `S3Client.UploadFile` to copy base64-encoded files into S3
- On submission of Paper LPAs, store `FileUpload` objects in S3 and return a reference to them in `File`
- Update the fixtures UI to support uploads

For CTC-146 #minor
  • Loading branch information
gregtyler committed May 10, 2024
1 parent eab1525 commit 82daaa9
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 7 deletions.
40 changes: 37 additions & 3 deletions docs/schemas/2024-10/donor-details.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,31 @@
"type": "string",
"enum": ["option-a", "option-b"]
},
"restrictionsAndConditions": {
"type": "string"
},
"signedAt": {
"type": "string",
"format": "date-time"
}
},
"if": {
"required": ["channel"],
"properties": {
"channel": { "const": "online" }
}
},
"then": {
"properties": {
"restrictionsAndConditions": {
"type": "string"
}
}
},
"else": {
"properties": {
"restrictionsAndConditionsImages": {
"$ref": "#/$defs/Images"
}
}
},
"additionalProperties": false,
"$defs": {
"Address": {
Expand Down Expand Up @@ -293,6 +310,23 @@
}
],
"type": "object"
},
"Images": {
"type": "array",
"items": {
"$ref": "#/$defs/ImageUpload"
}
},
"ImageUpload": {
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"data": {
"type": "string"
}
}
}
}
}
40 changes: 40 additions & 0 deletions fixtures/static/js/json-schema-editor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { Tabs as GovukTabs } from "../govuk-frontend.min.js";

const { Draft07 } = window.jlib;

const toBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(";base64,")[1]);
reader.onerror = reject;
});

export class JsonSchemaEditor {
/**
* @type {HTMLTextAreaElement}
Expand Down Expand Up @@ -186,6 +194,38 @@ export class JsonSchemaEditor {
$input.classList.add("govuk-input");

$parent.appendChild(this.createGovukFormGroup(nub, $input));
} else if (
schema.type === "object" &&
Object.keys(schema.properties).join(",") === "filename,data"
) {
const $filename = document.createElement("input");
$filename.type = "hidden";
$filename.name = `${pointer}/filename`;
const $data = document.createElement("input");
$data.type = "hidden";
$data.name = `${pointer}/data`;

const $input = document.createElement("input");
$input.id = `f-${pointer}`;
$input.type = "file";
$input.classList.add("govuk-file-upload");

$input.addEventListener("change", async () => {
const file = $input.files?.[0];
if (file) {
$filename.value = file.name;
$filename.dispatchEvent(new InputEvent("input", { bubbles: true }));

$data.value = await toBase64(file);
$data.dispatchEvent(new InputEvent("input", { bubbles: true }));
}
});

$parent.appendChild($filename);
$parent.appendChild($data);
$parent.appendChild(this.createGovukFormGroup("Upload file", $input));

parents[pointer] = document.createElement("div");
} else if (schema.type === "object" || schema.type === "array") {
const $details = document.createElement("details");
$details.classList.add("govuk-details");
Expand Down
36 changes: 36 additions & 0 deletions internal/objectstore/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package objectstore
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/ministryofjustice/opg-data-lpa-store/internal/shared"
)

type awsS3Client interface {
Expand Down Expand Up @@ -38,6 +42,38 @@ func (c *S3Client) Put(ctx context.Context, objectKey string, obj any) error {
return err
}

func (c *S3Client) UploadFile(ctx context.Context, image shared.FileUpload, path string) (shared.File, error) {
var imgData []byte
var err error

if imgData, err = base64.StdEncoding.DecodeString(image.Data); err != nil {
return shared.File{}, err
}

_, err = c.awsClient.PutObject(
ctx,
&s3.PutObjectInput{
Bucket: aws.String(c.bucketName),
Key: aws.String(path),
Body: bytes.NewReader(imgData),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
},
)

if err != nil {
return shared.File{}, err
}

hash := sha256.New()
hash.Write(imgData)

return shared.File{
Path: path,
Hash: hex.EncodeToString(hash.Sum(nil)),
}, nil

}

func NewS3Client(awsConfig aws.Config, bucketName string) *S3Client {
awsClient := s3.NewFromConfig(awsConfig)

Expand Down
77 changes: 77 additions & 0 deletions internal/objectstore/client_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package objectstore

import (
"bytes"
"context"
"errors"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/ministryofjustice/opg-data-lpa-store/internal/shared"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
Expand Down Expand Up @@ -32,3 +37,75 @@ func TestPut(t *testing.T) {
assert.Equal(t, nil, err)
client.AssertExpectations(t)
}

func TestUploadFile(t *testing.T) {
upload := shared.FileUpload{
Filename: "myfile.txt",
Data: "Q29udGVudHMgb2YgbXkgZmlsZQ==",
}

client := mockAwsClient{}
client.On("PutObject", mock.Anything, &s3.PutObjectInput{
Bucket: aws.String("bucket1"),
Key: aws.String("dir/myfile.txt"),
Body: bytes.NewReader([]byte("Contents of my file")),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
}).Return(&s3.PutObjectOutput{}, nil)

c := S3Client{
bucketName: "bucket1",
awsClient: &client,
}

file, err := c.UploadFile(context.Background(), upload, "dir/myfile.txt")

assert.Nil(t, err)
assert.Equal(t, "dir/myfile.txt", file.Path)
assert.Equal(t, "7ac4f2b48096ac5f4600a0775563d0f2b3369a3ea00d1fa813f45c18620dba28", file.Hash)
client.AssertExpectations(t)
}

func TestUploadFileDecodingError(t *testing.T) {
upload := shared.FileUpload{
Filename: "myfile.txt",
Data: "This isn't base 64",
}

c := S3Client{
bucketName: "bucket1",
awsClient: &mockAwsClient{},
}

file, err := c.UploadFile(context.Background(), upload, "dir/myfile.txt")

assert.Equal(t, file, shared.File{})
assert.Contains(t, err.Error(), "illegal base64 data")
}

func TestUploadFileS3Error(t *testing.T) {
upload := shared.FileUpload{
Filename: "myfile.txt",
Data: "Q29udGVudHMgb2YgbXkgZmlsZQ==",
}

expectedErr := errors.New("could not save object")

client := mockAwsClient{}
client.On("PutObject", mock.Anything, &s3.PutObjectInput{
Bucket: aws.String("bucket1"),
Key: aws.String("dir/myfile.txt"),
Body: bytes.NewReader([]byte("Contents of my file")),
ServerSideEncryption: types.ServerSideEncryptionAwsKms,
}).Return(&s3.PutObjectOutput{}, expectedErr)

c := S3Client{
bucketName: "bucket1",
awsClient: &client,
}

file, err := c.UploadFile(context.Background(), upload, "dir/myfile.txt")

assert.Equal(t, file, shared.File{})
assert.Equal(t, expectedErr, err)
client.AssertExpectations(t)
}
11 changes: 11 additions & 0 deletions internal/shared/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package shared

type File struct {
Path string `json:"path"`
Hash string `json:"hash"`
}

type FileUpload struct {
Filename string `json:"filename"`
Data string `json:"data"`
}
10 changes: 6 additions & 4 deletions internal/shared/lpa.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@ type LpaInit struct {
WhenTheLpaCanBeUsed CanUse `json:"whenTheLpaCanBeUsed,omitempty"`
LifeSustainingTreatmentOption LifeSustainingTreatment `json:"lifeSustainingTreatmentOption,omitempty"`
RestrictionsAndConditions string `json:"restrictionsAndConditions,omitempty"`
RestrictionsAndConditionsImages []FileUpload `json:"restrictionsAndConditionsImages,omitempty"`
SignedAt time.Time `json:"signedAt"`
CertificateProviderNotRelatedConfirmedAt *time.Time `json:"certificateProviderNotRelatedConfirmedAt,omitempty"`
}

type Lpa struct {
LpaInit
Uid string `json:"uid"`
Status LpaStatus `json:"status"`
RegistrationDate *time.Time `json:"registrationDate,omitempty"`
UpdatedAt time.Time `json:"updatedAt"`
Uid string `json:"uid"`
Status LpaStatus `json:"status"`
RegistrationDate *time.Time `json:"registrationDate,omitempty"`
UpdatedAt time.Time `json:"updatedAt"`
RestrictionsAndConditionsImages []File `json:"restrictionsAndConditionsImages,omitempty"`
}

type LpaType string
Expand Down
15 changes: 15 additions & 0 deletions lambda/create/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Store interface {

type S3Client interface {
Put(ctx context.Context, objectKey string, obj any) error
UploadFile(ctx context.Context, image shared.FileUpload, path string) (shared.File, error)
}

type Verifier interface {
Expand Down Expand Up @@ -104,6 +105,20 @@ func (l *Lambda) HandleEvent(ctx context.Context, req events.APIGatewayProxyRequ
data.Status = shared.LpaStatusProcessing
data.UpdatedAt = time.Now()

if data.Channel == shared.ChannelPaper && len(input.RestrictionsAndConditionsImages) > 0 {
data.RestrictionsAndConditionsImages = make([]shared.File, len(input.RestrictionsAndConditionsImages))
for i, image := range input.RestrictionsAndConditionsImages {
path := fmt.Sprintf("%s/scans/rc_%d_%s", data.Uid, i, image.Filename)

data.RestrictionsAndConditionsImages[i], err = l.staticLpaStorage.UploadFile(ctx, image, path)

if err != nil {
l.logger.Error("error saving restrictions and conditions image", slog.Any("err", err))
return shared.ProblemInternalServerError.Respond()
}
}
}

// save
if err = l.store.Put(ctx, data); err != nil {
l.logger.Error("error saving LPA", slog.Any("err", err))
Expand Down

0 comments on commit 82daaa9

Please sign in to comment.