はじめに
- GAE/Go で API サーバーを動かすにあたって管理画面を同一アプリケーション上で動かす方法について考えてみた
- 管理画面なので認証機能はほしい
- GAE のライブラリ
google.golang.org/appengine/user
を使えば Google アカウントで認証できる - 同ライブラリでは Google アカウントの権限によってアクセスレベルを設定できる
- 権限は Google Cloud Platform のコンソールで IAM と管理の項目からコントロールできる
- 権限の種別は管理者かそうでないかだけ
- IAM 上で App Engine のオーナーや管理者などの何らかの権限を与えると管理者扱いになる様子
- GAE のライブラリ
- 管理画面のコンテンツの例として Toml ファイルを読み込んで Datastore にインポートする画面を作ってみる
- GAE/Go の開発環境では Datastore の(エミュレータの)管理画面が貧弱でデータの追加が出来ないので地味に面倒
サンプルアプリのリポジトリ
使い方
- 管理画面へは
/admin/
でアクセスする - 管理画面のトップからログインする
- 開発環境だと任意のEメールアドレスで任意の権限でログインをして動作を確認できる
- 本番環境だとログイン中の Google アカウントでアプリケーションにアクセスするかどうかの確認画面が表示される
アプリの設定
-
/admin
配下を管理画面に/api
配下を API サーバーとする -
/admin/
にはログイン用のリンクを置くので誰でもアクセスできる -
/admin/.*
へのアクセスにはlogin: admin
によって管理者権限が必要になる
app.yaml
runtime: go
api_version: go1
handlers:
- url: /admin/
script: _go_app
- url: /admin/.*
script: _go_app
login: admin
- url: /api/.*
script: _go_app
アプリの実装
app.go
package app
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/naoina/toml"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/user"
"io/ioutil"
"net/http"
"text/template"
)
func init() {
r := mux.NewRouter()
// Management page
admin := r.PathPrefix("/admin").Subrouter()
admin.Path("/").HandlerFunc(adminIndexHandler)
admin.Path("/toml").HandlerFunc(adminTomlImportHander)
// API page
api := r.PathPrefix("/api").Subrouter()
api.Path("/").HandlerFunc(apiIndexHandler)
http.Handle("/", r)
}
// Top of management pages
func adminIndexHandler(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
u := user.Current(ctx)
tmpl := `
<!DOCTYPE html>
<html>
<head>
<title>Admin index page</title>
</head>
<body>
{{ if .User }}
Welcome, {{ .User }} (<a href="{{ .LogoutUrl }}">sign out</a>)
<h3>contents</h3>
<ul>
<li><a href="/admin/toml">Toml Importer</a></li>
</ul>
{{ else }}
<a href="{{ .LoginUrl }}">Sign in or register</a><br>
{{ end }}
</body>
</html>
`
t, err := template.New("").Parse(tmpl)
if err != nil {
panic(err)
}
loginUrl, _ := user.LoginURL(ctx, "/admin/")
logoutUrl, _ := user.LogoutURL(ctx, "/admin/")
data := struct {
User *user.User
LoginUrl string
LogoutUrl string
}{
User: u,
LoginUrl: loginUrl,
LogoutUrl: logoutUrl,
}
err = t.Execute(w, data)
if err != nil {
panic(err)
}
}
// Toml file importer
func adminTomlImportHander(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
if r.Method == "POST" {
// read uploaded file
f, _, err := r.FormFile("uploadfile")
if err != nil {
panic(err)
}
buf, err := ioutil.ReadAll(f)
if err != nil {
panic(err)
}
// struct of datadtore kind
type Table1 struct {
Id int64
Col1 string
Col2 int64
}
// struct of toml file
var input struct {
Tables []Table1
}
// parse toml
if err = toml.Unmarshal(buf, &input); err != nil {
panic(err)
}
// import toml data
for _, table := range input.Tables {
key := datastore.NewKey(ctx, "Table1", "", table.Id, nil)
if _, err := datastore.Put(ctx, key, &table); err != nil {
panic(err)
}
}
}
// render form
tmpl := `
<!DOCTYPE html>
<html>
<head>
<title>Admin toml importer</title>
</head>
<body>
<form enctype="multipart/form-data" action="/admin/toml" method="post">
<input type="file" name="uploadfile" />
<input type="submit" value="upload" />
</form>
</body>
</html>
`
t, err := template.New("").Parse(tmpl)
if err != nil {
panic(err)
}
t.Execute(w, nil)
}
// Api page (dummy)
func apiIndexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data := struct{ Message string }{Message: "hello world"}
json.NewEncoder(w).Encode(data)
}
インポートするファイル
table1.toml
[[Tables]]
Id = 1
Col1 = "string1"
Col2 = 100
[[Tables]]
Id = 2
Col1 = "string2"
Col2 = 200
[[Tables]]
Id = 3
Col1 = "string3"
Col2 = 300