サーバーレス(HTML on S3)にGoogle認証付きS3アップローダーを作る

### 更新(2017/11/18)
- S3にあげるたびに更新が起こるのを阻止
- HTML5のvalidationを使う
- アップロードが完了した場合内容を画面上部に表示

# やりたいこと
- S3上にHTMLとJSファイルをおいて静的webページとして公開
- webページに入力した内容をcsvにまとめてS3に出力
- 認証はgoogleのOpenID Connectを使用（roleをつけておく）
- バケットポリシーでIP制限をかけておく

# S3でwebページ公開に関して
以下のサイトを参考にして公開
[S3で静的ウェブサイトをホスティングしてみる](http://www.simpline.co.jp/tech/?p=1150)
バケットポリシーも作っておく

# googleのOpen Connect周り
以下のサイトを参考にクライアントID＋認証周り作成
[Googleの「OpenID Connect」を利用する為の「クライアントID」の取得方法](https://pg.kdtk.net/393)

<img width="941" alt="スクリーンショット 2017-11-13 18.23.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/198985/a0f3f949-5a98-0cd0-2d51-ceaf13cf6f6f.png">
＊承認済みのJavaScript生成元を設定せずにしばらくはまっていました...


# Googleと連携するroleの作成
AWSのコンソール画面で
IAM　>　role　>　roleの作成
と進み、ウェブIDのタブを選択してプロバイダーにGoogleを選択
AudienceにはクライアントIDを入力してpolicyのattachなどを行う
<img width="1339" alt="スクリーンショット 2017-11-15 16.49.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/198985/4c3e989f-6b6e-3ea4-45c5-d958c18ebe5b.png">

例で用いたpolicyはS3の特定のフォルダにのみアクセス可能なものにしました

```json:policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME/output/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME"
            ],
            "Effect": "Allow",
            "Condition": {
                "StringEquals": {
                    "s3:prefix": "output"
                }
            }
        }
    ]
}
```


# いよいよJavascript作成
フォルダ構成

- BUCKET
    - output
    - HTML
        - index.html
        - favicon.ico
        - logic.js

## ログイン周り
以下のURLを参考にJavascriptを作成（ログイン箇所に関しては例をそのまま使用）
[ブラウザの JavaScript](https://aws.amazon.com/jp/developers/getting-started/browser/)
[Web Federated Identity Examples](http://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/config-web-identity-examples.html)

```html:index.html
<!DOCTYPE html>
<html>
<head>
    <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
    <title>AWS SDK for JavaScript - Sample Application</title>
    <meta charset="utf-8"/>
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</head>

<body>
<div id="results" role="alert"></div>
<nav class="navbar navbar-default">
    <div class="container">
        <!-- 2.ヘッダ情報 -->
        <div class="navbar-header">
            <a class="navbar-brand">S3アップローダー</a>
        </div>
    </div>
</nav>


<div class="container" id="form">
    <form>
        <div class="form-group" id="name">
            <label>名前</label>
            <input type="text" name="name" class="form-control" placeholder="申請　太郎" required>
        </div>
        <div class="form-group" id="account_flg">
            <label>アカウント</label>
            <select name="account_flg" class="form-control">
                <option value="不要">不要</option>
                <option value="必要">必要</option>
            </select>
        </div>
        ...
        <button type="button" onclick="upload_csv();" class="btn btn-primary">申請登録</button>
    </form>
</div>
<span
        id="login"
        class="g-signin"
        data-height="short"
        data-callback="loginToGoogle"
        data-cookiepolicy="single_host_origin"
        data-requestvisibleactions="http://schemas.google.com/AddActivity"
        data-scope="https://www.googleapis.com/auth/plus.login">
</span>
<script>
    //以下ほぼサンプルのコピペ
    //s3 = null;  s3をアップローダーでも使いたいので後々グローバル変数で指定しています
    var clientID = '************.apps.googleusercontent.com'; // Google client ID
    var roleArn = 'arn:aws:iam::************:role/ROLE_NAME';
    document.getElementById('login').setAttribute('data-clientid', clientID);
    function loginToGoogle(response) {
        if (!response.error) {
            AWS.config.credentials = new AWS.WebIdentityCredentials({
                RoleArn: roleArn,
                WebIdentityToken: response.id_token
            });
            s3 = new AWS.S3();
            console.log('You are now logged in.');
        } else {
            console.log('There was a problem logging you in.');
        }
    }
    (function () {
        var po = document.createElement('script');
        po.type = 'text/javascript';
        po.async = true;
        po.src = 'https://apis.google.com/js/client:plusone.js';
        var s = document.getElementsByTagName('script')[0];
        s.parentNode.insertBefore(po, s);
    })();
</script>
<script src="logic.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>
```

## アップローダー周り

```js:logic.js
//ここに必要な項目のIDを書いておく
csv_array = [
    'name',
    'account_flg'
]

//メインのところ（どのcsvを作成するか）
function upload_csv() {
　　 //連続で投稿する場合に結果画面を一度空欄にする
    document.getElementById('results').innerHTML = "";
    //inputタグを指定してvalidation確認
    var validity_list = document.getElementById("form").getElementsByTagName("div");
    for (var i = 0, l = validity_list.length; i < l; i++) {
        var validity_zone = validity_list[i].getElementsByTagName("input")[0];
        if (!validity_zone) {
            continue;
        }
        if (!validity_zone.checkValidity()) {
            alert('【'+validity_list[i].getElementsByTagName("label")[0].innerHTML + '】 '+ validity_zone.validationMessage);
            return;
        }
    }
    //成功・失敗検知用
    success_flg = true;
    upload('account', make_csv(csv_array));
    //終わった後上部に移動して結果をalertで通知
　　 $("html,body").animate({scrollTop: 0}, 'fast');
    if (success_flg) {
        alert('Success upload');
    } else {
        alert("Fail upload ...");
    }
    //画面更新を防ぐ
    return false;
}

//配列情報から変数(csv)にヘッダー情報と内容を入れる関数
function make_csv(array) {
    var csv = '';
    for (i in array) {
        var block = document.getElementById(array[i]);
        csv += block.getElementsByTagName("label")[0].innerHTML + ',';
    }
    csv = csv.slice(0,-1);
    csv += '\n';
    for (i in array) {
        var block = document.getElementById(array[i]);
        if (block.getElementsByTagName("select")[0]){
            var num = block.getElementsByTagName("select")[0].selectedIndex;
            csv += block.getElementsByTagName("select")[0].options[num].value + ',';
        } else {
            csv += block.getElementsByTagName("input")[0].value + ',';
        }
    }
    csv = csv.slice(0,-1);
    return csv;
}

//出来上がったcsvファイルをアップロードする関数
function upload(service_name, body) {
    var results = document.getElementById('results');
    var params = {
        Bucket: 'BUCKET_NAME',
        Key: 'output/' + service_name + '_' + timestamp() + '.csv', //名前が被らないようにtimestam付
        ContentType: 'csv',
        Body: body
    };
    s3.putObject(params, function (err, data) {
        if (err) {
            success_flg = false;
        }
        //実行結果を画面上部に表示
        results.setAttribute("class", !err ? 'alert alert-success':'alert alert-danger' );
        if (!err){
            results.innerHTML = results.innerHTML + "<strong>【"+service_name+"】</strong>";
        }else{
            results.innerHTML = results.innerHTML + "<strong>ERROR【"+service_name+"】</strong>";
        }
    });
}

//タイムスタンプ作成用関数
function timestamp() {
    var d = new Date();
    var year = d.getFullYear();
    var month = d.getMonth() + 1;
    var day = d.getDate();
    var hour = ( d.getHours() < 10 ) ? '0' + d.getHours() : d.getHours();
    var min = ( d.getMinutes() < 10 ) ? '0' + d.getMinutes() : d.getMinutes();
    return '' + year + month + day + hour + min;
}
```

# 終わりに
IP制限をかけておけば簡単に社内サービスを作成できます
単にローカルファイルを上げるだけであれば、htnl上でファイル取得をしてuploadの関数を使えばできます
サーバーレス素晴らしい