Cognitoユーザー認証によるS3 のAccess制御をAmplifyとReactとDropzoneを使って実装する。(その4)

Cognitoユーザー認証によるS3 のAccess制御をAmplifyとReactとDropzoneを使って実装する。(その3)」の続きです。

npm start
します。Browseが起動して、Login画面が表示されます。画面中付近に、 Upload Fileを入力できるボタンが表示されます。

S3のBucketが空であることを確認しておきます。Management ConsoleからS3を開き、upload Bucketをクリックします。

Web Applicationに戻って、FileをUploadします。 「Browse」をクリックして、Fileを指定します。今回はTiff Fileのみ選択できるようにしてあるので、適当なTiff Fileを指定して、「開く」をクリックします。

指定したFileが表示されていることを確認して、「Upload File」をクリックします。Uploadできず、Errorが表示されます。Loginしていない状態では、FileのUploadが出来ないことを確認しました。

再度、S3のUpload Bucketが空であることを確認しておきます。

Loginして、同様にFileをUploadします。Fileは指定されたままなので、「Upload File」をクリックします。もちろん「Browse」をクリックして、Fileを再指定しても良いです。FileがUploadできました。

S3にFileがUploadされているか確認します。Management ConsoleからS3を開き、Upload Bucketをクリックします。Bucketの内容を表示している画面のままでしたら、更新のIconをクリックします。「protected」というFolderが作成されています。「protected」をクリックします。「ap-northeast-1:」で始まるFolderが作成されています。そのFolderをクリックします。「upload」というFolderが作成されています。「upload」をクリックします。UploadしたFileが見えます。今回は、AmplifyのStorageのLevelにProtectedを指定したので、上記のような階層にFileがUploadされます。Policyを使ってS3へのAccess制御を指定しましたが、Amplify StorageのLevelとPolicyを細かく指定することによって、User間でお互いにFileを見れるようにしたり、他のUserからは見れないようにしたりすることが可能です。

この方法ではUploadするFileを一つ一つ指定してUploadする必要があります。それでは不便なため、複数のFileを一気にUploadできるようにするためにDropZoneを使ってUploadするFileを指定できるようにします。まず、react-dropzoneをInstallします。
cd upload
npm install -save react-dropzone

App.jsを編集してDropZoneからUpload Fileを指定するように変更します。

import React, { useState, useCallback, useMemo }  from 'react';
import { useDropzone } from 'react-dropzone';
import Storage from "@aws-amplify/storage";
import { SetS3Config } from "./services";
import logo from './logo.svg';
import './App.css';
const baseStyle = {
  flex: 1,
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'center',
  padding: '20px',
  borderWidth: 2,
  borderRadius: 2,
  borderColor: '#eeeeee',
  borderStyle: 'dashed',
  backgroundColor: '#fafafa',
  color: '#bdbdbd',
  outline: 'none',
  transition: 'border .24s ease-in-out'
};
const activeStyle = {
  borderColor: '#2196f3'
};
const acceptStyle = {
  borderColor: '#00e676'
};
const rejectStyle = {
  borderColor: '#ff1744'
};
function App(props) {
  const [response, setResponse] = useState("");
  const onDrop = useCallback(acceptedFiles => {
    acceptedFiles.forEach(file => {
      const reader = new FileReader();
      reader.onload = () => {
        const fileAsArrayBuffer = new Uint8Array(reader.result);
          // do whatever you want with the file content
        SetS3Config(process.env.REACT_APP_bucket_name, "protected");
        Storage.put('upload/' + file.name, fileAsArrayBuffer, { contentType: file.type })
        .then(result => {
            console.log(result);
            setResponse("File is uploaded!");
            return true;
        })
        .catch(err => {
            console.log(err);
            setResponse(`File is not uploaded: ${err}`);
            return true;
        });
      };
      reader.onabort = () => {
        console.log("Reading file was aborted");
        setResponse("Reading file was aborted");
      }
      reader.onerror = () => {
        console.log("Reading file has failed");
        setResponse("Reading file has failed");
      }
      reader.readAsArrayBuffer(file);
    });
  }, []);
  const {acceptedFiles, getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject} = useDropzone({onDrop, accept: 'image/tif, image/tiff'});
  const files = acceptedFiles.map(file => (
    <div align="center">
      {file.path} - {file.size} bytes
    </div>
  ));
  const style = useMemo(() => ({
    ...baseStyle,
    ...(isDragActive ? activeStyle : {}),
    ...(isDragAccept ? acceptStyle : {}),
    ...(isDragReject ? rejectStyle : {})
  }), [
    isDragActive,
    isDragAccept,
    isDragReject
  ]);
  if(props.authState === 'signedIn') {
    return (
      <div className="App">
        <div {...getRootProps({style})}>
          <input {...getInputProps()} />
          <p>Drag 'n' drop some files here, or click to select files</p>
        </div>
        <div>
          <h4>Files</h4>
          <p>{files}</p>
        </div>
        {!!response && <div>{response}</div>}
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  } else if(props.authState === 'confirmSignUp') {
    return (
      <div className="Your account is not granted">
        <header className="App-header">
          Your account is not granted.
        </header>
      </div>
    );
  } else if(props.authState === 'signIn') {
    return (
      <div className="Welcome to the world">
        <header className="App-header">
          Welcome to the world.
        </header>
      </div>
    );
  } else {
    return (
      <div className="Login failed">
        <header className="App-header">
          Login failed.
        </header>
      </div>
    );
  }
}
export default App;

App.jsの編集が完了したら、
npm start
します。 Browseが起動して、Login画面が表示されたら、Loginします。Loginすると、UploadするFileをDrag&Dropする領域が出来ています。Uploadする複数のFileをDrag&Dropします。Drag&Dropする領域をクリックして、File Browserから複数のFileを指定することもできます。Uploadが完了したら、Management ConsoleからS3を開き、UploadされたFileを確認します。

これで、 File Upload機能 の実装は完了です。

それでは、以前と同様にS3にDeployしてみましょう。
package.jsonにhomepageのentryが設定されているか確認します。
cd upload
npm run build
aws s3 sync --exact-timestamps --delete --acl public-readdirname bucketname

Management ConsoleからS3を開き、export.hacya.comのindex.htmlのURLを確認して、BrowserでそのURLを開きます。同様にTiff FileをUploadします。

Uploadできました。これで完了です。

Cognitoユーザー認証によるS3 のAccess制御をAmplifyとReactとDropzoneを使って実装する。(その3)

Web Applicationを実装します。「Reactを使ってStaticなWeb Hostingをする。」と同じように、まずは、Reactのひな型を作ります。
create-react-app upload
cd upload
npm install -save aws-amplify aws-amplify-react

前回作成、または、変更したFileを参考に、S3の設定を追加しながら、作成したひな形にCopyします。詳細な説明は、「Cognitoユーザー認証をAmplifyとReactを使って実装する。(その3)」に戻って確認してください。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import AppWithAuth from './AppWithAuth';
import * as serviceWorker from './serviceWorker';

//Import Amplify Configure function
import { configureAmplify } from './services'

configureAmplify();

ReactDOM.render(<AppWithAuth />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

services.jsでは、S3の設定を追加します。また、SetS3Config()というfunctionを定義します。BucketやS3のAccess Levelを変更した後、AmplifyのStorage.Configure()をCallして、Amplifyの設定を変更する必要があるためです。

import Amplify from '@aws-amplify/core';
import Auth from '@aws-amplify/auth';
import Storage from '@aws-amplify/storage';

export function configureAmplify() {
  Amplify.configure(
  {
    Auth: {
      region: process.env.REACT_APP_region,
      userPoolId: process.env.REACT_APP_userPoolId,
      userPoolWebClientId: process.env.REACT_APP_userPoolWebClientId,
      identityPoolId: process.env.REACT_APP_identityPoolId,
    },
    Storage: {
      AWSS3: {
        bucket: process.env.REACT_APP_bucket_name,
        region: process.env.REACT_APP_region,
      }
    }

  } );
}

//Configure Storage with S3 bucket information
export function SetS3Config(bucket, level){
  Auth.configure({
    region:  process.env.REACT_APP_region,
    identityPoolId: process.env.REACT_APP_identityPoolId,
  });
  Storage.configure({ 
    bucket: bucket,
    level: level,
  });
}

projectのroot directoryの “.env”にS3の設定を追加した、5つの情報を設定ます。
REACT_APP_region=ap-northeast-1
REACT_APP_userPoolId=ap-northeast-1yourPoolID
REACT_APP_userPoolWebClientId=yourWebClientId
REACT_APP_identityPoolId=ap-northeast-1:yourIdentityPoolId
REACT_APP_bucket_name=yourBucketName

AppWithAuth.js、MyVerifyContact.js、MyConfirmSignUp.jsは変更ありません。

import React from 'react';
import { Authenticator, ConfirmSignIn, RequireNewPassword, ConfirmSignUp, VerifyContact, ForgotPassword, TOTPSetup } from 'aws-amplify-react';
import App from "./App";
import MyVerifyContact from "./MyVerifyContact"
import MyConfirmSignUp from "./MyConfirmSignUp"

const signUpConfig = {
  hideAllDefaults: true,
  signUpFields: [
    {
      label: 'Username',
      key: 'username',
      required: true,
      placeholder: 'Username',
      displayOrder: 1
    },
    {
      label: 'Password',
      key: 'password',
      required: true,
      placeholder: 'Password',
      type: 'password',
      displayOrder: 2
    }
  ]
}

function AppWithAuth() {

  function handleAuthStateChange(state, data) {
    if (state === 'confirmSignIn') {
        /* Do something when the user has signed-in */
    }
  }

  return (
    <Authenticator signUpConfig={signUpConfig} hide={[ConfirmSignIn, RequireNewPassword, VerifyContact, ConfirmSignUp, ForgotPassword, TOTPSetup]} onStateChange={handleAuthStateChange}>
      <MyVerifyContact override={VerifyContact} />
      <MyConfirmSignUp override={ConfirmSignUp} />
      <App />
    </Authenticator>
  )
}

export default AppWithAuth;
import { AuthPiece } from 'aws-amplify-react';

class MyVerifyContact extends AuthPiece {
  constructor(props) {
    super(props);
    this._validAuthStates = ["verifyContact"];
    this.state = {}
  }

  static getDerivedStateFromProps(props, state) {
    if(props.authState === "verifyContact") {
      props.onStateChange('signedIn');
      return props;
    } else {
      return null;
    }
  }

  showComponent(theme) {
    return null;
  }

}

export default MyVerifyContact;
import React from 'react';

import { I18n } from 'aws-amplify';
import { AuthPiece } from 'aws-amplify-react';

import {
  FormSection,
  SectionHeader,
  SectionBody,
  SectionFooter,
  Button
} from 'aws-amplify-react';

class MyConfirmSignUp extends AuthPiece {
  constructor(props) {
    super(props);
    this._validAuthStates = ["confirmSignUp"];
    this.gotoSignIn = this.gotoSignIn.bind(this);
  }

  gotoSignIn() {
    super.changeState('signIn');
  }

  showComponent(theme) {
    return (
      <FormSection theme={theme}>
        <SectionHeader theme={theme}>{I18n.get('Please contact your administrator to grant your account.')}</SectionHeader>
        <SectionBody theme={theme}>
        </SectionBody>
        <SectionFooter theme={theme}>
          <Button onClick={this.gotoSignIn} theme={theme}>
            {I18n.get('Goto SignIn')}
          </Button>
        </SectionFooter>
      </FormSection>
    );

  }

}

export default MyConfirmSignUp;

最後にApp.jsを編集して、S3へUploadできるようにします。

import React, { useState, createRef }  from 'react';
import logo from './logo.svg';
import Storage from "@aws-amplify/storage";
import { SetS3Config } from "./services";
import './App.css';

function App(props) {

  const [uploadFile, setUploadFile] = useState({});
  const [response, setResponse] = useState("");
  const uploadRef = createRef();
  const onInputClick = () => {
    if(uploadRef.current) {
      uploadRef.current.value = null;
      uploadRef.current.click();
    }
    setResponse("");
  }

  function uploadFileToS3() {
    SetS3Config(process.env.REACT_APP_bucket_name, "protected");
    Storage.put(`upload/${uploadFile.name}`,
                uploadFile,
                { contentType: uploadFile.type })
      .then(result => {
        console.log(result);
        setUploadFile({});
        setResponse("File is uploaded!");
      })
      .catch(err => {
        console.log(err);
        setResponse(`File is not uploaded: ${err}`);
      });
  };

  function InputUploadFile(props) {
    return(
      <div  align="center">
        <input
          type="file"
          style={{ display: "none" }}
          accept="image/tif, image/tiff"
          ref={uploadRef}
          onChange={(e) => setUploadFile(e.target.files[0])}
        />
        <input value={uploadFile.name} placeholder="Select file" />
        <button onClick={onInputClick}> Browse </button>
        <button onClick={uploadFileToS3}> Upload File </button>

        {!!response && <div>{response}</div>}
      </div>
    )
  }

  if(props.authState === 'signedIn') {
    return (
      <div className="App">
        <InputUploadFile />

        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  } else if(props.authState === 'confirmSignUp') {
    return (
      <div className="Your account is not granted">
        <header className="App-header">
          Your account is not granted.
        </header>
      </div>
    );
  } else if(props.authState === 'signIn') {
    return (
      <div className="Welcome to the world">
        <InputUploadFile />

        <header className="App-header">
          Welcome to the world.
        </header>
      </div>
    );
  } else {
    return (
      <div className="Login failed">
        <header className="App-header">
          Login failed.
        </header>
      </div>
    );
  }
}

export default App;

npm startからの説明は次回。