LoginSignup
5
9

More than 5 years have passed since last update.

GAE/Go で API サーバーと管理画面を同一アプリケーションで動かす

Last updated at Posted at 2016-11-21

はじめに

  • GAE/Go で API サーバーを動かすにあたって管理画面を同一アプリケーション上で動かす方法について考えてみた
  • 管理画面なので認証機能はほしい
    • GAE のライブラリ google.golang.org/appengine/user を使えば Google アカウントで認証できる
    • 同ライブラリでは Google アカウントの権限によってアクセスレベルを設定できる
    • 権限は Google Cloud Platform のコンソールで IAM と管理の項目からコントロールできる
    • 権限の種別は管理者かそうでないかだけ
    • IAM 上で App Engine のオーナーや管理者などの何らかの権限を与えると管理者扱いになる様子
  • 管理画面のコンテンツの例として 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
5
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
9