Google Apps ScriptのAdmin SDKでユーザ作成フォームを作る【GAS】
G Suiteを運用しているといつも面倒に感じてるのが、G Suiteの管理画面での作業。ブラウザ上で様々な設定やユーザ管理などなどを行う管理者用のページなんですが、インターフェースがいちいち使いにくい上に、どこに何があるのかが非常に分かりにくいサービスです。また、実際にはユーザを追加するといっても、同姓同名がいたり、どこの施設所属などの情報を持てないので、結局スプレッドシートに書き出して追加なんて羽目になっていたりします。
また、このページは他のGoogle系サービスのサイトと異なり、admin指定のアカウント以外入れないので、切り替えてログインしなければいけなかったりします。当然ユーザを追加したら、申請者に通知もしてあげなければならず、メールで送るなどの作業も必要でしょう。という具合に、1ユーザ追加するといっても、煩わしい事この上ない。作成するアカウント量が多ければ多いほど、それはやっかいになり膨大な時間を消費する事になります。
ということで、申請~承認~アカウント作成~メールで申請者に通知(PDFで書類を添付)~メールアドレス一覧に書き込み、までをGoogle Apps Script上でやってしまおうという事から作り、現在活用しています。その際に必要になるのが、Admin SDK。当たり前ですが、実行するには管理者権限が必要になります。自分は自分に限定管理者の権限を付与しているので、アカウント切り替えせずにGAS上でそのまま作業を行っています。
今回使用するスプレッドシート類
事前準備
Adminページでの作業
APIアクセス許可
Adminにログインしたら以下の設定を施して、Admin SDKからの操作を許可してあげます。
- Adminコンソールにログインする
- ホーム画面下の「その他の設定」をクリックする
- 「セキュリティ」の項目があるので、クリックする
- 真ん中あたりに「APIリファレンス」があるのでクリックして開く
- APIアクセスを有効にするにチェックを入れる。これで完了
図:下の方に設定項目が隠れています。
図:APIアクセスを許可してあげましょう。
作成者へ権限付与
特権管理者ですべての作業を行うのは懸命なこととは言えません。すべての操作が可能になってしまいます。通常の担当者にユーザの追加や削除などの権限程度を付与してあげましょう。アカウント切り替えてスクリプトを実行するといった面倒が生じなくなります。
- Adminコンソールにログインする
- ホーム画面より、「ユーザ」をクリックする
- 権限付与したいユーザを探し出してクリックします。
- 管理者の権限と役割という項目があるので、クリックして開きます。
- 今回はここで「ユーザ管理者」の割当状況をオンにしてあげます。もう少し下の権限でも良いでしょう。
これで権限付与は完了です。特権管理者じゃなくとも、担当ユーザの権限でスクリプトを実行してユーザの作成が可能になります。
図:管理者の権限を付与してあげる
図:ユーザ管理者権限で作成が可能
プロジェクトを移動
APIを有効にする
申請フォームスプレッドシートを開き、スクリプトエディタに入り、APIを有効化します。
- スクリプトエディタのメニューより「リソース」⇒「Googleの拡張サービス」を開きます。
- Admin Directory APIを有効化します。
- 続けて、「Google Cloud Platform API ダッシュボード」をクリックします。
- APIとサービスにて、「APIとサービスの有効化」をクリックする
- admin sdkと検索すると出てくるので、クリックします。
- 有効化をクリックします。
- 認証情報の作成は不要です
図:Admin Directory APIが必要
図:Admin SDKを有効化しましょう。
Picker用のAPIキーを入手しセットする
APIを有効にする
認証情報を作る
※APIキーを入手したら、ソースコード内のPicker.html3種類に其のキーをセットしてあげてください。
ソースコード
申請フォームのコード
HTML側コード(workflow.html)
<head> <link rel="stylesheet" href="https://officeforest.org/wp/library/icons/drugman.css" /> <link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/themes/smoothness/jquery-ui.css" /> <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css"> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.min.js"></script> <style type="text/css"> table , td, th { border: 0px solid #595959; border-collapse: collapse; } td, th { padding: 3px; height: 25px; text-align: left; } th { background: #f0e6cc; } .even { background: #fbf8f0; } .odd { background: #fefcf9; } .ui-autocomplete { max-height: 200px; overflow-y: auto; overflow-x: hidden; padding-right: 20px; } #jquery-ui-autocomplete label { float: left; margin-right: 0.5em; color: black; font-size: 15px; } </style> <script> var nowuser; //現在のユーザのアドレス var hospman = ""; //施設名リストを格納 var mailist = ""; //発行済メールアドレス一覧を格納 var domains = ""; //ドメイン一覧を保存する //ツールチップを表示する処理 jQuery( function() { jQuery(document).tooltip({ position: { my: 'left top', at: 'right bottom', collision: 'none' } }); }); //起動時に自動的に実行しプルダウンメニューを用意する google.script.run.withSuccessHandler(onSuccess).formman(); google.script.run.withSuccessHandler(onSuccess2).maildata(); google.script.run.withSuccessHandler(onSuccess3).getdomains(); //施設名リストを生成する function onSuccess(data){ var json = JSON.parse(data); hospman = json; //セレクトボックスを生成する var html = "<select title='プルダウンより選択' id='hosp1'><option>項目を選択して下さい</option>"; var html2 = "<select title='プルダウンより選択' id='hosp2'><option>項目を選択して下さい</option>"; var hlength = json.length; for(var i = 0;i<hlength;i++){ html += "<option>" + json[i] + "</option>"; html2 += "<option>" + json[i] + "</option>"; } //selectオプションの下を入れる html += "</select><p>"; html2 += "</select><p>"; //nodeに挿入する document.getElementById("hosplist").innerHTML = html; document.getElementById("hosplist2").innerHTML = html2; } //発行済メールアドレス一覧データを取得する function onSuccess2(data){ var json = JSON.parse(data); mailist = json; console.log(mailist); } //ドメイン設定を取得してリストを作成する function onSuccess3(data){ var json = JSON.parse(data); domains = json; //リストを作成する var html = "<select id='domain' title='ドメインを選択'>"; var jlength = json.length; for(var i = 0;i<jlength;i++){ html += "<option>" + json[i] + "</option>"; } html += "</select>"; //セレクトボックスを反映する document.getElementById("seldom").innerHTML = html; } //フォームを送信するメインルーチン function disp(){ //データのValidation作業 var valitar = ""; var valitar2 = ""; var editarray = []; var arraylength = ""; //フォームの入力内容の回収 valitar = document.getElementById("wasabi1").value; if(valitar == ""){ alert("あなたの氏名が入っていませんよ"); document.getElementById("wasabi1").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("hosp1").value; if(valitar == "項目を選択して下さい"){ alert("申請者の施設名が選択されていませんよ。"); document.getElementById("hosp1").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("wasabi2").value; if(valitar == ""){ alert("あなたの所属部署名が入っていませんよ。"); document.getElementById("wasabi2").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("wasabi3").value; if(valitar == ""){ alert("あなたの役職名が入っていませんよ。"); document.getElementById("wasabi3").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementsByName("s3"); //アカウント申請区分を判定する for(var i=0; i<valitar.length; i++){ if(valitar[i].checked){ var hantei = valitar[i].value; editarray.push(hantei); } } // 個人アカウントの場合には値を取得する if(hantei == "個人アカウント"){ valitar = document.getElementById("wasabi4").value; if(valitar == ""){ alert("使用者の姓が入っていませんよ。"); document.getElementById("wasabi4").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("wasabi5").value; if(valitar == ""){ alert("使用者の名が入っていませんよ。"); document.getElementById("wasabi5").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("wasabi6").value; if(valitar == ""){ alert("使用者のセイ(カタカナ)が入っていませんよ。"); document.getElementById("wasabi6").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("wasabi7").value; if(valitar == ""){ alert("使用者のナマエ(カタカナ)が入っていませんよ。"); document.getElementById("wasabi7").focus(); return 0; }else{ editarray.push(valitar); } }else{ //部門アカウントの場合は姓名の入力はせず空でpush editarray.push(""); editarray.push(""); editarray.push(""); editarray.push(""); } valitar = document.getElementById("hosp2").value; if(valitar == "項目を選択して下さい"){ alert("使用者の施設名が選択されていませんよ。"); document.getElementById("hosp2").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("wasabi8").value; if(valitar == ""){ alert("使用者の所属部署名が入っていませんよ。"); document.getElementById("wasabi8").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("wasabi9").value; if(valitar == ""){ alert("希望アカウント名が入っていませんよ。"); document.getElementById("wasabi9").focus(); return 0; }else{ //ドメインを取得する var domdom = document.getElementById("domain").value; //メールアドレスを合成する var kibou = valitar + "@" + domdom; //希望アカウント名の内容チェック if(MailCheck(kibou)){ }else{ alert('メールアカウントの形式が正しくない可能性があります・・。'); document.getElementById("wasabi9").focus(); document.getElementById("wasabi9").select(); return; } //ドメイン含めたアドレスをpushする editarray.push(kibou); } valitar = document.getElementById("wasabi10").value; if(valitar == "項目を選択して下さい"){ alert("アカウント使用目的が選択されていませんよ。"); document.getElementById("wasabi10").focus(); return 0; }else{ editarray.push(valitar); } valitar = document.getElementById("wasabi11").value; if(valitar == ""){ alert("申請理由が入っていませんよ。"); document.getElementById("wasabi11").focus(); return 0; }else{ editarray.push(valitar); } //希望アカウントが作れるかどうかチェック var mlength = mailist.length; for(var i = 0;i<mlength;i++){ if(kibou == mailist[i][13]){ alert("入力した希望アカウント名は既に使われています。"); return 0; } } if(window.confirm("入力した情報を持って、GSアカウントの申請を行いますか?")){ //プログレスインディケータを表示 document.getElementById("kurukuru").style.display = "none"; document.getElementById("progress").style.display = "block"; //host側に承認プロセスを渡す google.script.run.withSuccessHandler(onSend).telepon(editarray); }else{ window.alert('処理はキャンセルされました'); } } //送信結果を受信する function onSend(data){ var json = JSON.parse(data); //プログレスインディケータを元に戻す document.getElementById("kurukuru").style.display = "block"; document.getElementById("progress").style.display = "none"; //判定によって処理を分岐 if(json[0] == "OK"){ //フォームの内容をクリアする for(var i = 1;i<10;i++){ var node = "wasabi" + i; console.log(node); document.getElementById(node).value = ""; } //プルダウン系の項目をデフォルト値に変更する document.getElementById("wasabi10").value = "項目を選択して下さい"; document.getElementById("hosp1").value = "項目を選択して下さい"; document.getElementById("hosp2").value = "項目を選択して下さい"; document.getElementById("wasabi11").value = ""; document.getElementById("domain").value = ""; } //メッセージを表示する alert(json[1]); } //メールアドレスチェック用関数 function MailCheck( mail ) { var mail_regex1 = new RegExp( '(?:[-!#-\'*+/-9=?A-Z^-~]+\.?(?:\.[-!#-\'*+/-9=?A-Z^-~]+)*|"(?:[!#-\[\]-~]|\\\\[\x09 -~])*")@[-!#-\'*+/-9=?A-Z^-~]+(?:\.[-!#-\'*+/-9=?A-Z^-~]+)*' ); var mail_regex2 = new RegExp( '^[^\@]+\@[^\@]+$' ); if( mail.match( mail_regex1 ) && mail.match( mail_regex2 ) ) { // 全角チェック if( mail.match( /[^a-zA-Z0-9\!\"\#\$\%\&\'\(\)\=\~\|\-\^\\\@\[\;\:\]\,\.\/\\\<\>\?\_\`\{\+\*\} ]/ ) ) { return false; } // 末尾TLDチェック(〜.co,jpなどの末尾ミスチェック用) if( !mail.match( /\.[a-z]+$/ ) ) { return false; } return true; } else { return false; } } //アクセスコントロール function a_ctrl(){ var valitar = document.getElementsByName("s3"); //アカウント申請区分を判定する for(var i=0; i<valitar.length; i++){ if(valitar[i].checked){ var hantei = valitar[i].value; break; } } //コントロールするパーツ var test = document.getElementById("wasabi4"); var test2 = document.getElementById("wasabi5"); var test3 = document.getElementById("wasabi6"); var test4 = document.getElementById("wasabi7"); //アカウントタイプ判定する if(hantei == "部門アカウント"){ test.disabled = true; test2.disabled = true; test3.disabled = true; test4.disabled = true; test.style.backgroundColor = '#A4A4A4'; test2.style.backgroundColor = '#A4A4A4'; test3.style.backgroundColor = '#A4A4A4'; test4.style.backgroundColor = '#A4A4A4'; }else{ test.disabled = false; test2.disabled = false; test3.disabled = false; test4.disabled = false; test.style.backgroundColor = '#fff'; test2.style.backgroundColor = '#fff'; test3.style.backgroundColor = '#fff'; test4.style.backgroundColor = '#fff'; } } </script> </head> <div id='userinfo' style='color:#ffffff;'></div> <div width="95%" id='kurukuru' style="display:block;"> <div style='padding:3px 5px;border-color:#74f442;border-width:0 0 1px 7px;border-style:solid;background:#F8F8F8;'>担当者情報登録</div><p> <div><label><b>担当者の氏名</b></label></div> <INPUT type='text' class='wasabi' id='wasabi1' placeholder='あなたの氏名を入力して下さい。' size='30' title='今このフォームを入力してる人の名前'><p> <div><label><b>担当者の所属施設名</b></label></div> <div id="hosplist"><img border="0" src="https://officeforest.org/wp/library/ProgressSpinner.gif" width="20" height="20"></div> <div><label><b>担当者の所属部署</b></label></div> <INPUT type='text' class='wasabi' id='wasabi2' placeholder='あなたの所属部署' size='30' title='あなたの所属部署(例:総務部)を入力してください。'><p> <div><label><b>担当者の役職</b><font color='blue'>(役職がない場合は、なしと入力)</font></label></div> <INPUT type='text' class='wasabi' id='wasabi3' placeholder='あなたの役職名' size='25' title='あなたの役職名を入力してください。'><p> <div style='padding:3px 10px;border-color:#74f442;border-width:0 0 1px 7px;border-style:solid;background:#F8F8F8;'>アカウントタイプ</div><p> <div class="sample" style="width:410px;"title="申請する区分を選択して下さい。"> <input type="radio" name="s3" id="select1" value="個人アカウント" checked='checked' onclick='a_ctrl();'> <label for="select1"> 個人アカウント</label> <input type="radio" name="s3" id="select2" value="部門アカウント" onclick='a_ctrl();'> <label for="select2">部門アカウント</label> </div> <br><br><br> <div style='padding:3px 10px;border-color:#74f442;border-width:0 0 1px 7px;border-style:solid;background:#F8F8F8;'>アカウント使用者の情報</div><p> <Table border='0' cellspacing='0' width='250' cellpadding='0'> <Tr> <Td style='border-style: none;'> <b>姓</b></Td><Td style='border-style: none;'><b>名</b> </Td> </Tr> <Td style='border-style: none;'> <INPUT type='text' class='wasabi' id='wasabi4' placeholder='姓を入力' size='10' title='申請対象者の姓です。'> </Td> <Td style='border-style: none;'> <INPUT type='text' class='wasabi' id='wasabi5' placeholder='名を入力' size='10' title='申請対象者の名です。'> </Td> </Table> <Table border='0' cellspacing='0' width='250' cellpadding='0'> <Tr> <Td style='border-style: none;'> <b>セイ</b></Td><Td style='border-style: none;'><b>ナマエ</b> </Td> </Tr> <Td style='border-style: none;'> <INPUT type='text' class='wasabi' id='wasabi6' placeholder='セイを入力' size='15' title='申請対象者のカタカナの姓です。'> </Td> <Td style='border-style: none;'> <INPUT type='text' class='wasabi' id='wasabi7' placeholder='ナマエを入力' size='15' title='申請対象者のカタカナの名です。'> </Td> </Table> <p> <div><label><b>使用者の所属施設名</b></label></div> <div id="hosplist2"><img border="0" src="https://officeforest.org/wp/library/ProgressSpinner.gif" width="20" height="20"></div> <div><label><b>使用者の所属部署</b></label></div> <INPUT type='text' class='wasabi' id='wasabi8' placeholder='使用者の所属部署' size='30' title='使用者の所属部署名を入力'><p> <div><label><b>希望アカウント名</b> - <font color='blue'>英数字、小文字で記入をし、@以下は選択します。</font></label></div> <input type="text" name="sampleName" id='wasabi9' placeholder='希望アカウント名を入力!!' size='50' title='名前の部分だけ'> <b>@</b> <span id="seldom"><img border="0" src="https://officeforest.org/wp/library/ProgressSpinner.gif" width="20" height="20"></span> <p> <font color="blue"> 例)世田谷太郎さんの場合 setagaya.taro@xxx.hoge.jp<br><br> 【個人アカウントについて】<br> ・ローマ字で姓名をピリオドで区切ります。<br> 〔姓〕.〔名〕@xxx.xx.jp<br> </font> <div><label><b>アカウント使用目的</b></label></div> <div class='block form-group'> <select id='wasabi10' title='使用目的を選択してください。'> <option selected>項目を選択して下さい</option> <option>メール:グループ内</option> <option>メール:グループ外</option> <option>GoogleDrive</option> <option>その他</option> </select> </div><p> <div style='padding:3px 10px;border-color:#74f442;border-width:0 0 1px 7px;border-style:solid;background:#F8F8F8;'>申請理由 - <label><font color='red'>業務上、どのような理由によりアカウントが必要なのか、詳細な理由を明記(「業務で使用するため」のみの申請は不可)</font></label></div><p> <TEXTAREA class='wasabi' cols='80' rows='11' placeholder='申請の理由などを入力してください。' id='wasabi11' title='申請する理由を完結にまとめて入力して下さい。'></TEXTAREA><p> <div align='center' id='kinokoman'> <hr> <p><button class='action' onClick='disp()'>フォームを送信</button></p> </div> </div> <div id="progress" style="display:none;"> <center> <img border="0" src="https://officeforest.org/wp/library/icons/spinner.gif"> <p> <b><div style="color:red; font-size:10pt;">現在データ送信中...しばらくそのままお待ち下さい!!</div></b> </center> </div>
- 申請部分はそのほとんどが、申請内容のValidationやGAS側へのデータの送信が占めています。
- 内容は長いコードではありますが、シンプルです。
- ユーザが使いやすいように、JavaScriptの様々なライブラリを活用して利便性を向上しましょう。
GAS側コード(telepon関数)
//承認実行処理(所属長用) function telepon(senddata){ //エラートラップ用の変数 var errorword = ""; var errorblock = ""; var sendmsg = ""; var mailman = ""; //ロックサービスの宣言 var lock = LockService.getPublicLock(); try{ //30秒間のロックを開始 lock.waitLock(30000); //書き込み用変数を準備 errorword = "書込用の準備段階でエラーが発生しました。"; errorblock = "1"; var Properties = PropertiesService.getScriptProperties(); var sheetid = Properties.getProperty("editsheet"); var sheet = SpreadsheetApp.openById(sheetid); var ss = sheet.getSheetByName("送信ログ"); var mtitle = sheet.getRangeByName("mtitle").getValues(); var editday = getDate2(new Date()); var gamaster = Properties.getProperty("systemad"); //CSSデータを取得する var css = HtmlService.createHtmlOutputFromFile('css') .setSandboxMode(HtmlService.SandboxMode.IFRAME).getContent(); //現在ログイン中のユーザ名を取得しておく var nowuser = GetUser(); //所属施設名を取得する var hospname = senddata[1]; //書き込み用配列を用意 var editsheet = []; var datalength = senddata.length; //IDを生成して取得する errorword = "固有ID生成エラー"; errorblock = "2"; var Properties = PropertiesService.getScriptProperties(); var uid = Number(Properties.getProperty('UniqueID')) + Number(1); Properties.setProperty('UniqueID',uid); uid = uid + "_" + uidgene(); //とりあえずIDと日付をpush errorword = "書き込み用データ準備エラー"; errorblock = "3"; editsheet.push(uid); editsheet.push(editday); //受信データをpush var slength = senddata.length; for(var i=0;i<slength;i++){ editsheet.push(senddata[i]); } //担当者のmailアドレスや申請中ステータスをpush editsheet.push(nowuser); editsheet.push(""); editsheet.push("申請中"); //書き込み用配列からシートへレコード追加 errorword = "スプレッドシート書き込みエラー"; errorblock = "4"; ss.appendRow(editsheet); // 入力項目を本文に埋め込む errorword = "メール本文自動生成エラー"; errorblock = "5"; var formbody = css + "<body><table class='type08'><thead><tr>" + "<th>項目名</th>" + "<th>入力値</th></tr></thead><tbody>"; for (var j = 0; j < slength; j++){ //入力値を反映 formbody = formbody + "<tr>"; formbody = formbody + "<th scope='row'><b>" + mtitle[0][j] + "</b></th>"; formbody = formbody + "<td>" + senddata[j] + "</td>"; //テーブルを閉じる formbody = formbody + "</tr>"; } formbody = formbody + "</tbody></table><body>"; // 件名、本文の設定 //改行するには <br>や<p> を入れてください var header = "<br><br>送信した内容は以下の通りです。<br><br>──────────────────────────────────<br><br>" + formbody; //本文のヘッダー★5 var footer = "申請が承認されましたら、発行通知と共に「申請者」に対してメールで通知がなされます。その際には、添付の書類を" + "発行対象者にお渡しいただき、必ずパスワードの変更をお願い致します。<br>"; var bodytop = "「GAアカウント申請」を受け付けました。<br>" + "確認・承認後、改めてご連絡致します。<br><br>" + "【連絡事項】<br>" + "・本メールには返信をしないで下さい。<br>"; var body =""; //本文 body += header; //件名作成と文面のレイアウト調整 var subject = "【GAアカウント申請】No." + uid + "_" + hospname + "_" + getDate(editday); //件名★4 body += footer; body = bodytop + body; //申請者に対する自動応答メール送信 errorword = "自動応答メール送信エラー"; errorblock = "6"; MailApp.sendEmail({ to: nowuser, subject: subject, htmlBody: body, name:"GAアカウントの申請", noReply:true, }); //承認者に対しては、申請が来た旨のメールを通知する。 var body2 = nowuser + "【" + hospname + "】より、下記の内容でGAアカウントの申請がなされました。<p><br>" + formbody + "<br><p>"; //処理担当者にメールを送信 MailApp.sendEmail({ to: gamaster, subject: hospname + "よりGAアカウントの申請", htmlBody: body2, name:"GAアカウント発行の申請と作成依頼", }); //ロックを解除する lock.releaseLock(); //フォーム側に完了通知を送る var array = ["OK","無事に申請が完了しました。"]; return JSON.stringify(array); }catch(e){ var checkword = "ロックのタイムアウト: 別のプロセスがロックを保持している時間が長すぎました。"; //ロックされていてタイムアウトした場合の処理 if(e.message == checkword){ //ロックエラーの場合 sendmsg = "<center><img src='https://officeforest.org/wp/library/forms/img62495029.jpg'><p><b>サービスがタイムアウトしました。もう一度、やり直して下さい。</b></p></center><p>"; }else{ //通常のエラーが発生した場合の処理 //ロックを解除する lock.releaseLock(); //エラー発生時の処理 //エラーメッセージの組み立て sendmsg = "エラータイトル:" + errorword + "<br>"; sendmsg += "エラー発生箇所:" + errorblock + "<br><br>"; sendmsg += "作業は途中で終了していますので、はじめからやり直して下さい。入力済内容は承認者にもメールで通知しています。<br><br>"; } //エラーメール用入力データの準備 var css = HtmlService.createHtmlOutputFromFile('css') .setSandboxMode(HtmlService.SandboxMode.IFRAME).getContent(); var formbody = css + "<body><table class='type08'><thead><tr>" + "<th>項目名</th>" + "<th>入力値</th></tr></thead><tbody>"; var slength = senddata.length; for (var j = 0; j < slength; j++){ //入力値を反映 formbody = formbody + "<tr>"; formbody = formbody + "<th scope='row'><b>" + mtitle[0][j] + "</b></th>"; formbody = formbody + "<td>" + senddata[j] + "</td>"; //テーブルを閉じる formbody = formbody + "</tr>"; } formbody = formbody + "</tbody></table><body>"; //エラーメッセージを追加する sendmsg += e.message; //メッセージと入力内容をマージする mailman = sendmsg + formbody; //エラーメールを通知する MailApp.sendEmail({ to: GetUser(), cc: gamaster, subject: "GAアカウント新規申請登録作業エラー通知", htmlBody: mailman, name:"新規申請登録作業ERROR", }); //エラー通知をフォーム側へ送る sendmsg = sendmsg.replace(/<br>/g,"\n"); var array = ["NG",sendmsg]; return JSON.stringify(array); } }
- 今回のフォームは申請者側にも管理者側にもメールで作成申請内容が自動応答で通知されます。
- 通知されるメールはレスポンシブメールとして送られるので、スマフォでも見やすいものになっています。
- 内容は送信ログシートに記録されます。
ユーザ追加コマンドのコード
HTML側コード
<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css"> <link rel="stylesheet" href="https://code.jquery.com/ui/1.11.4/themes/cupertino/jquery-ui.css" /> <link rel="stylesheet" href="https://officeforest.org/wp/library/forms/jquery.alerts.css" type="text/css" /> <script type="text/javascript" src="https://apis.google.com/js/api.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script> <script type="text/javascript" src="https://officeforest.org/wp/library/forms/jquery.alerts.js"></script> <style type="text/css"> .kousin { overflow-y: auto; width:98%; height:350px; padding:5px; border:2px dotted #ffffff; color:#000000; background-color:#ffffff; line-height:1.5em; margin-left: auto; margin-right: auto; } p#domino{ text-shadow: 1px 1px 2px #000; font-size: 120%; } div.section { width: 680px; /* ボックスの幅 */ padding-bottom: 1px; /* ボックスの下パディング */ font-size: 100%; /* ボックスの文字サイズ */ background-color: #E0ECF8; /* ボックスの背景色 */ -moz-border-radius: 15px; /* Firefox */ -webkit-border-radius: 15px; /* Safari,Chrome */ border-radius: 15px; /* CSS3 */ } /* --- 見出しエリア --- */ div.section div.heading { margin: 0 0 1em; /* 見出しエリアのマージン(上、左右、下) */ padding: 5px; /* 見出しエリアのパディング */ border-left: 4px #add8e6 solid; /* 見出しの左境界線 */ line-height: 100%; background: #3f3f3f url(head2.gif) repeat-x top; /* 見出しエリアの背景 */ border: 1px #666666 solid; /* 見出しエリアの境界線 */ font-size: 120%; color:#ffffff; //-moz-border-radius: 20px; /* Firefox */ //-webkit-border-radius: 20px; /* Safari,Chrome */ //border-radius: 20px; /* CSS3 */ -webkit-border-radius: 15px 15px 0px 0px; -moz-border-radius: 15px 15px 0px 0px; border-radius: 15px 15px 0px 0px; } div.section div.heading input[type=radio]:checked { background: #FFFF00; } div.section div.heading input[type=radio]:checked + label { color: #FFFF00; font-weight: bold; } /* --- 見出し --- */ div.section div.heading:hover { background-color: #cccccc; color: #000000; } /* --- ボックス内の段落 --- */ div.section p.test { margin: 1em 10px; /* 段落のマージン(上下、左右) */ } textarea.wasabi{ background-color: #bde9ba; } </style> <script type="text/javascript"> //グローバル変数 var selectid = ""; //jQuery Alert Dialogのバグ回避の為のコード jQuery.browser = {}; (function () { jQuery.browser.msie = false; jQuery.browser.version = 0; if (navigator.userAgent.match(/MSIE ([0-9]+)\./)) { jQuery.browser.msie = true; jQuery.browser.version = RegExp.$1; } }) (); //自動実行される関数 google.script.run.withSuccessHandler(onUser).GetUser(); google.script.run.withSuccessHandler(onSuccess).userlength(); google.script.run.withSuccessHandler(onSuccess2).futureacc(); function onUser(data){ document.getElementById("users").innerHTML = "<font color='red'><b>現在、" + data + "にてログイン中"; } function onSuccess(data){ var intman = parseInt(data) document.getElementById("kinoko").innerHTML = "<b>現在の作成済ユーザ総数:<font color='blue'>" + intman + "</font>名<b>"; } //作成対象になるアカウントのリストを生成して表示 function onSuccess2(data){ var json = JSON.parse(data); document.getElementById("mlist").innerHTML = json; } function disp(){ //選択中のラジオボタンのIDを取得して返す var arr = document.getElementsByName('a'); //コメント欄のテキストデータを取得する selectid = ""; for(var i=0;i<arr.length;i++){ if(arr[i].checked) { selectid = arr[i].value; break; } } //未選択時の動作 if(selectid == ""){ jAlert("選択されてませんよ", 'アラート'); return 0; } if(window.confirm("GS申請No." + selectid + "を承認しますか?")){ //プログレス表示に切り替える document.getElementById("makeman").style.display = "none"; document.getElementById("progman").style.display = "block"; //host側に承認プロセスを渡す console.log(selectid); google.script.run.withSuccessHandler(retadd).telepon3(selectid,"承認"); }else{ jAlert("処理はキャンセルされました", 'アラート'); } } function disp2(){ //選択中のラジオボタンのIDを取得して返す var arr = document.getElementsByName('a'); //現在選択してるレコードデータを取得する selectid = ""; for(var i=0;i<arr.length;i++){ if(arr[i].checked) { selectid = arr[i].value; break; } } //未選択時の動作 if(selectid == ""){ jAlert("選択されてませんよ", 'アラート'); return 0; } // 「OK」時の処理開始 + 確認ダイアログの表示 if(window.confirm("GS申請No." + selectid + "を却下しますか?")){ //プログレス表示に切り替える document.getElementById("makeman").style.display = "none"; document.getElementById("progman").style.display = "block"; //host側に承認プロセスを渡す google.script.run.withSuccessHandler(retadd).telepon3(selectid,"却下"); }else{ jAlert("処理はキャンセルされました", 'アラート'); } } function retadd(data){ //データを取得する var json = JSON.parse(data); //フラグデータを取得する //1 = 作業完了 0 = エラー発生 var flag = json[2]; //プログレス表示を解除する document.getElementById("makeman").style.display = "block"; document.getElementById("progman").style.display = "none"; //フラグに基づき処理を分岐 if(flag == 0){ //エラー発生時の処理 //サーバーからのリターン処理を表示する jAlert(json[1], 'エラー発生!!'); return 0; }else{ //無事に処理が完了した場合の処理 //サーバーからのリターン処理を表示する jAlert(json[1], '処理完了通知'); //特定のエレメントを削除する var dom_obj = document.getElementById(selectid); dom_obj.parentNode.removeChild(dom_obj); } } </script> <div id="kinoko"></div> <div id="users"></div> <hr> <div class="kousin" id="mlist"></div> <hr> <div align="center" id="makeman" style="display:block;"> <button class="action" onClick="disp()">承認</button> <button class="create" onclick="disp2()">却下</button> </div> <div align="center" id="progman" style="display:none;"> <center><b><font color="blue">アカウント作成中!!・・・</font></b><img border='0' src='https://officeforest.org/wp/library/ProgressSpinner.gif' width='32' height='32'></center> </div>
- ほとんどのコードは申請フォーム同様、申請中ステータスになっているものをリストアップしてダイアログに表示します。
- 承認の場合の処理と却下の場合の処理はそれぞれ、telepon3という同じ関数に引数でわかるように渡しています。
GAS側コード
//アカウント承認画面を出す function makeAccount(){ //スプレッドシートデータの取得 var sheet = SpreadsheetApp.getActiveSpreadsheet(); var ui = SpreadsheetApp.getUi(); //フィルタされたシートを取得 var ss = sheet.getSheetByName("申請中"); //データを取得する. var dataman = ss.getRange("A2:S").getValues(); var datalength = dataman.length; //#N/Aだった場合の処理。 if(dataman[0][0] == "#N/A"){ ui.alert("データの件数が0件でしたよ"); return 0; } var sheet = SpreadsheetApp.getUi(); var output = HtmlService.createTemplateFromFile('makelist'); var html = output.evaluate().setWidth(710).setHeight(500).setSandboxMode(HtmlService.SandboxMode.IFRAME); sheet.showModelessDialog(html, "アカウント作成画面"); } //現在の作成済みアカウント総数を返す function userlength(){ var Properties = PropertiesService.getScriptProperties(); var msheet = Properties.getProperty("editsheet"); var sheet = SpreadsheetApp.openById(msheet).getSheetByName("発行済").getDataRange().getValues() ; var slength = sheet.length - 1; return slength; } //作成予定のアカウントリストを作成して返す function futureacc(){ //このシートのIDを取得 var Properties = PropertiesService.getScriptProperties(); var msheet = Properties.getProperty("editsheet"); var html = ""; //申請中データを取得する var sheet = SpreadsheetApp.openById(msheet).getSheetByName("申請中").getRange("A2:S").getValues(); var dataLength = sheet.length; //現在、申請中のレコードをピックアップして整形して表示 for(var i = 0;i<dataLength;i++){ //ステータスが「申請中」のものだけピックアップ if(sheet[i][18] != "申請中"){ //スルーする continue; } //必要な情報を取得して、HTMLを組み立てて出力する html += "<div class='section' id=" + sheet[i][0] +"><div class='heading'><input type='radio' name='a' value=" + sheet[i][0] + ">"; html += "<label>GS申請No." + sheet[i][0] + " - " + sheet[i][6] + "</label></div>"; html += "<p class='test'><b>希望アカウント:</b>" + sheet[i][13] + "(所属施設:" + sheet[i][3] + ")"; html += "</p></div><p>"; } //生成したHTMLデータを返す return JSON.stringify(html); } //アカウント作成処理 function telepon3(id, approve){ var Properties = PropertiesService.getScriptProperties(); var systemad = Properties.getProperty("systemad"); var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("送信ログ"); var range = sheet.getRange("A2:S").getValues(); var datalength = range.length; var counter = 1; var flag = ""; var retmsg = ""; //エラートラップ用の変数 var errorword = ""; var errorblock = ""; var msg = ""; //エラーメッセージ用 //ロックサービスの宣言 var lock = LockService.getPublicLock(); try{ //30秒間のロックを開始 lock.waitLock(30000); //IDを探索し該当レコードの場所を特定する errorword = "対象レコードが見つかりませんでした。"; errorblock = "1"; //idがヒットする行を探索 for(var i = 0;i<datalength;i++){ counter = counter + 1; if(range[i][0] == id){ //該当のレコードのデータを配列に格納する var targetrecord = sheet.getRange("A" + counter + ":S" + counter).getValues(); for(var j = 0;j<targetrecord.length;j++){ sheetdata.push(targetrecord[j]); } //部門判定エラー errorword = "部門判定が出来ませんでした。"; errorblock = "2"; //部門判定 var bumon = sheetdata[0][6]; var nameman = ""; if(bumon == "部門アカウント"){ nameman = sheetdata[0][11] + sheetdata[0][12]; }else{ nameman = sheetdata[0][7] + sheetdata[0][8]; } if(approve == "却下"){ //却下通知 errorword = "却下通知のメール送信が出来ませんでした。"; errorblock = "3"; //却下通知メールを送信する var mailbody = sheetdata[0][2] + "様<br><br>" + "Googleアカウント作成担当でございます。<br><br>" + "申請内容を確認致しました結果、" + nameman + "様のGSアカウント申請は却下となりました。" + "詳細をご案内致しますので、お手数ですが下記の番号までお電話下さい。<br><br>" + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><br>" + "連絡先: Googleアカウント作成担当<br>" + "電話番号: 03-1259-9644<br>" + "受付: 9:00~17:00(平日のみ)<br>" + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><br><br>" + "よろしくお願いいたします。"; MailApp.sendEmail({ to: sheetdata[0][16], subject: "GSアカウントの申請について - " + getDate(new Date()), htmlBody: mailbody, cc: systemad, name:"GSアカウント申請シートからの送信", }); //ステータスを返す status = "OK"; flag = "1"; retmsg = "GSアカウントの却下処理完了しました。" var array = [status,retmsg,flag]; return JSON.stringify(array); } break; } } //申請者のメールアドレスを格納する var sinsei = sheetdata[0][13]; //発行通知 errorword = "発行通知作成エラー"; errorblock = "4"; //GAアカウント発行通知書の作成とIDの取得 var ret = docsgenerator(id); //ファイルをPDFへ変換してfileidを取得する var pdfname = DriveApp.getFileById(filesId).getName(); //最終rangeに書き込みがされていない場合、無限ループ var rangechk = SpreadsheetApp.openById(filesId).getRange("passwords").getValue(); while (rangechk == ""){ rangechk = SpreadsheetApp.openById(filesId).getRange("passwords").getValue(); } //発行通知 errorword = "PDF作成エラー"; errorblock = "5"; //PDF生成するURLをfetchする var pdfid = forpdfsheet(filesId,pdfname); var pdfman = "https://drive.google.com/file/d/" + pdfid; //発行通知 errorword = "作成PDFへのアクセス権限追加エラー"; errorblock = "6"; //作成したPDFに申請者のパーミッションを追加する var temppdfsheet = DriveApp.getFolderById(pdfid).addEditor(sinsei); //発行通知 errorword = "ステータス変更エラー"; errorblock = "7"; //ヒットした行の特定のカラムにドキュメントURLを入力する sheet.getRange("R" + counter).setValue(pdfman); sheet.getRange("S" + counter).setValue(approve); //発行通知 errorword = "承認通知メール送信エラー"; errorblock = "8"; //承認通知メールを送る var mailbody = sheetdata[0][2] + "様<br><br>" + "おつかれさまです。Googleアカウント作成担当でございます。<br><br>" + "ご申請いただいておりました" + nameman + "様のGoogleアカウントにつきまして" + "アカウント作成が完了いたしましたのでご連絡いたします。<br><br>" + "下記のURLにアカウント名及び仮パスワードが記載されておりますので、お手数ですがアカウントユーザーご本人にお渡しいただきますようお願い申し上げます。<br><br>" + "PDFのURL:" + pdfman + "<br><br>" + "不具合やご不明な点がございましたら、ご連絡ください。<br><br>" + "<font color='red'><b>尚、添付のPDFを別の方にメールで渡す場合には、一度添付のPDFはダウンロードし、改めてメールを作成し添付して送信するようにして下さい。</b></font><hr><br><br>"; //メール送信 MailApp.sendEmail({ to: sheetdata[0][16], subject: "GAアカウントの申請について - " + getDate(new Date()), htmlBody: mailbody, cc: systemad, name:"GAアカウント申請シートからの送信", }); //ロックを解除する lock.releaseLock(); //ステータスを返す status = "OK"; flag = "1"; retmsg = "GSアカウントの作成処理完了しました。" var array = [status,retmsg,flag]; return JSON.stringify(array); }catch(e){ //ロックを解除する lock.releaseLock(); var checkword = "ロックのタイムアウト: 別のプロセスがロックを保持している時間が長すぎました。"; //ロックされていてタイムアウトした場合の処理 if(e.message == checkword){ //ロックエラーの場合 var msg = "<center><img src='https://officeforest.org/wp/library/forms/img62495029.jpg' alt='エラー画像' width='100' height='100'><p><b>サービスがタイムアウトしました。もう一度、やり直して下さい。</b></p></center><p>"; }else{ //エラーメッセージの組み立て msg += "<center><img src='https://officeforest.org/wp/library/forms/img62495029.jpg' alt='エラー画像' width='100' height='100'><p><b>エラーが発生しました。</b></p></center><p>"; msg += "エラータイトル:" + errorword + "<br>"; msg += "エラー発生箇所:" + errorblock + "<br><br>"; msg += "作業は途中で終了していますので、はじめからやり直して下さい。<br><br></p>" msg += "<b><font color='red'>【エラーの詳細】<br>" + e.message + "</font></b>"; } //エラー通知をフォーム側へ送る var status = "NG"; flag = "0"; var array = [status,msg,flag]; return JSON.stringify(array); } } //発行通知をPDFに変換してIDを返す関数 function forpdfsheet(fileman,filename) { //アクティブシートのIDとGIDを取得する var ss = SpreadsheetApp.openById(fileman); var sheet = ss.getSheetByName("発行通知"); var sheetID = sheet.getSheetId(); var key = ss.getId(); var token = ScriptApp.getOAuthToken(); var Properties = PropertiesService.getScriptProperties(); var pdfdir = Properties.getProperty("pdfdir"); //PDF生成するURLをfetchする var url = "https://docs.google.com/spreadsheets/d/"+key+"/export?gid="+sheetID+"&format=pdf&portrait=true&size=A4&gridlines=false&fitw=true"; //10秒間スリープさせる Utilities.sleep(10000); //PDF化する var pdf = UrlFetchApp.fetch(url, {headers: {'Authorization': 'Bearer ' + token}}).getBlob().setName(filename + ".pdf"); var pdffolder = DriveApp.getFolderById(pdfdir); var pdffile = pdffolder.createFile(pdf); var pdfid = pdffile.getId(); return pdfid; } //アカウントを作成し、デフォルトパスワードを返す関数 function addUser(mail, firstname, familyname) { //アカウント作成メインルーチン var pass = String("gs_" + Math.random().toString(36).slice(-11)); var user = { primaryEmail: mail, name: { givenName: firstname, familyName: familyname }, password: pass, changePasswordAtNextLogin: true }; user = AdminDirectory.Users.insert(user); return pass; }
- telepon3が作成のルーチンではあるが、今回のテーマである「Admin SDK」にて管理者サイトに対して、実際にアカウント作成の命令を投げているのは、addUser関数である。
- 作成した内容はTemplateに従い通知書を作成、addUserで管理コンソールで自動生成されたパスワードを取得されたものを追記しています。
- 作成が無事に完了すると、通知されステータスは承認もしくは却下が記録されます。
実行と結果
本フォームおよび作成の為には事前に作業が必要です。以下の手順で作業をしましょう。
サイドバーの設定
- スプレッドシートメニューより「作業用」⇒「設定」を開く
- 管理者通知アドレスには、申請されてきたときにメールが届く先のアドレスです。
- ファイル格納場所として、フォルダを適当に作り、Google Drive内のフォルダをPickerで選択しましょう。GSアカウントの通知書が発行される場所です。
- PDF格納場所として、3.と同じく作業をします。通知書をPDF化してメールで送ります。
- 発行通知Templateとして、今回使用するファイルのTemplateを指定します。
この作業を行わないと、フォームが機能しません。
ウェブアプリケーションとして公開
申請フォームをオープンにしないと、誰も申請することができません。そこで、以下の作業をしておきましょう。
- スクリプトエディタのメニューより「公開」⇒「ウェブアプリケーションとして導入」を実行
- プロジェクトバージョンはNew、次のユーザとしてアプリケーションを実行は、作成者の自分で良いでしょう。
- アプリケーションにアクセスできるユーザは、通常はドメイン内の全員。外部に公開する必要はないので、全員や匿名ユーザの選択はする必要はありません。
- 更新ボタンを押すと、最後にexecのついたURLが生成されるので、これをGoogle Siteなどに貼り付けましょう。
セッティング
今回のフォームは以下のセッティングが必要です。
- スプレッドシートの設定シートにて、ドメインリストに発行する所持しているドメインをリストアップします。1組織で複数のドメインをG Suiteに持っている場合には複数記述しましょう。
- スプレッドシートの施設名リストには、例えば本店や支店などの発行管理単位を記述しましょう。申請者がどこに所属しており、どこの施設のアカウントを発行したいのか?申請時に自動このリストを読み取って選択肢として組み立てます。
作成手順
- 申請フォームから個人・部門アカウントでユーザは申請をすると、管理者に通知が来ます。申請中ステータスで送信ログシートに記録されます。
- 管理者は通知が来たら、スプレッドシートのメニューより「作業用」⇒「GSアカウントの承認・作成」を実行
- ダイアログが出るので、作成するアカウントのラジオボタンをクリックして選択。
- 承認、もしくは却下ボタンをクリックする
- 承認の場合、通知書が作成されaddUser関数でAdmin SDKが実行されてアカウント作成、PDF化されたファイルが添付されて、申請者に対してメール通知されます。
- 作成されると承認ステータスになり、発行済には自動的にリストが登録され、以降同じメアドでの発行をしようとしてもプログラム中で存在確認されるので、エラーを防ぐ事ができます。
図:アカウント作成用ダイアログ
ポイント
- 申請用スクリプトやフォームを別途作成して、スプレッドシートに必要事項を申請者に入れさせるようにしましょう。そのデータをこのユーザ作成スクリプトで利用します。
- 自分はユーザリストを別にスプレッドシートに持たせてるので、申請フォームでまず同一アドレスがあるかどうか?チェックさせています。
- Admin SDKで現在のユーザ一覧を取得はできますが、一応制限もあるので、あまり多用はしないように。
- 承認フォーム&スクリプト側に上記のコードを入れてあります。承認を実行すると、ユーザの追加が実行されるわけです。
- 自分は、ユーザ追加が実行がされたら、申請スプレッドシートの該当データをユーザリストにappendRowしてます。
- 今回はランダム生成したパスワードをこちらで設定するようにしています。それらを元に通知用テンプレートスプレッドシートに情報を書き込み、PDF化してメールで申請者に送信しています。
- Admin SDKは基本、管理者用ページで出来る事のかなりのことが、スクリプトで実行できるようになっていますので、応用すると結構いろいろ半自動化が出来ると思います。