前回のおさらい
前回Part1とPart1.5で環境構築を(ようやく)終わらせる事ができました。
今回から、いよいよコードゴリゴリ書いていこうと思います。
と、言いつつもDB、リダイレクト、テンプレートエンジン等、考えることが多いため少しづつ解決していきましょう。
前回からの変更点(先に読んでいただけると幸いです)
gooseのリポジトリ先変更
bitbucket経由でインストールしたgooseなのですが、どうもbitbucketのソースコードの更新が滞っている模様なため、githubリポジトリにフォークされたプロジェクトの方をインストールするように変更しました(そもそもbitbucketが使いづらいってのもあったのですが...)。
それに伴い、GomfileとGomfile.lockの内容を変更しました。
gom 'github.com/pressly/goose/cmd/goose'
gom 'github.com/pressly/goose/cmd/goose'
こちらのForkリポジトリですが、まだyamlファイルによるDB接続のサポートを行っていないため、gooseコマンドを打つ際に直接DB指定をするようにしています。
大変お手数ですが、一旦bitbucketリポジトリのgooseからgithubリポジトリにforkされたgooseを入れ直して以降の記事をお読みください。
GOPATH, vendoringについて
docker-composeでmain.goを実行する際にgom経由でインストールしたパッケージ群へのPATHがおかしなことになっていたため、調べていると以下のような記事を見つけました。
https://h3poteto.hatenablog.com/entry/2016/11/30/231652
Go 1.6以降はvendoring機能により、プロジェクトの直下にvendor
ディレクトリがあった場合はimport文の依存解決順序が、 vendor > $GOROOT > $GOPATHという順序でパッケージを探してくれる模様です。
つまりは、vendoring機能によってimportの依存解決をvendoring優先にするためにはプロジェクトの配置を$GOPATH
に置かなければいけません。
この事はgopathのhelpにも書かれていました。
go help gopath
Vendor Directories
Go 1.6 includes support for using local copies of external dependencies
to satisfy imports of those dependencies, often referred to as vendoring.
Code below a directory named "vendor" is importable only
by code in the directory tree rooted at the parent of "vendor",
and only using an import path that omits the prefix up to and
including the vendor element.
と言うことで、プロジェクトのディレクトリ構成を以下のように書き換えました。
├── Dockerfile
├── LICENSE
├── README.md
├── bin
│ └── gom
├── docker-compose.yml
└── src
├── dairy_report
│ ├── Gomfile
│ ├── Gomfile.lock
│ ├── db
│ │ ├── dbconf.yml
│ │ └── migrations
│ │ └── 20180930203149_create_users.sql
│ ├── main.go
│ ├── templates
│ │ └── login.ace
│ └── vendor
│ ├── bin
│ │ ├── ace
│ │ └── goose
更に、docker-composeで構築したGOコンテナのデフォルトのGOPATHは/go
なため、direnvで環境変数の設定考えていくと煩雑化していく恐れ大でした。そのため、デフォルトのGOPATHでアプリケーションが配置されるようにDockerfileを書き換えました。
FROM golang:1.11.0
RUN apt update
# PostgreSQLのClientをインストール
RUN apt install -y postgresql-client
RUN mkdir -p /go/src/dairy_report/db
RUN cd /go/src/dairy_report
ADD src/dairy_report/Gomfile Gomfile
ADD src/dairy_report/Gomfile.lock Gomfile.lock
ADD src/dairy_report/db/dbconf.yml db/dbconf.yml
# gomだけgo getで取得
RUN go get github.com/mattn/gom
ADD . /go
WORKDIR /go/src/dairy_report
RUN gom install
最後に.dockerignoreを以下のように書き直しました。
.envrc
bin/
src/dairy_report/vendor/
以上により、無事Goコンテナを無事起動することができました。
ようやくこれで第一歩って所ですね。
gooseによるテーブル作成
gooseでmigrationファイルを作成します。まずはUserテーブルを作成しましょう。
% goose create create_users
goose: created
dairy_report/db/migrations/20180930203149_create_users.go
% mv dairy_report/db/migrations/20180930203149_create_users.go dairy_report/db/migrations/20180930203149_create_users.sql
-- +goose Up
CREATE TABLE users (
id int NOT NULL PRIMARY KEY,
username text,
password text
);
-- +goose Down
DROP TABLE users;
とりあえず、UPでマイグレーションを実行するため、Usersテーブルを作成しユーザ名とパスワードを持たせるようにしました(一からこういうの作ろうとすると何が必要か調べなきゃいけないのが結構しんどいですね、、、まぁ仕方ないですが)
% goose postgres "host=$DAIRY_REPORT_DEVELOPMENT_HOST user=$DAIRY_REPORT_DEVELOPMENT_USERNAME password=$DAIRY_REPORT_DEVELOPMENT_PASSWORD dbname=$DAIRY_REPORT_DEVELOPMENT_DATABASE sslmode=disable" up
2018/09/30 14:41:52 OK 20180930203149_create_users.sql
2018/09/30 14:41:52 goose: no migrations to run. current version: 20180930203149
# psql -hdb -Udairy_report -ddairy_report_development
dairy_report_development=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------------+----------+--------------
public | goose_db_version | table | dairy_report
public | goose_db_version_id_seq | sequence | dairy_report
public | users | table | dairy_report
(3 rows)
dairy_report_development=# ~
dairy_report_development-# \d users
Table "public.users"
Column | Type | Modifiers
----------+---------+-----------
id | integer | not null
username | text |
password | text |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
続いてフォームを作成するためにテンプレートエンジンを見ていくことにします。
HTMLテンプレートエンジンを利用してslimテンプレートを読み込む
Goの標準ライブラリとしてhtml/template
があります。
こちらは、テンプレートを生成して書き込む事も出来ますが、テンプレートファイルを読み込む事もできます。
package main
import (
"log"
"net/http"
"html/template"
)
func handler(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("templates/login.html")
if err != nil {
log.Fatal(err)
}
err = tmpl.Execute(w, "")
if err != nil {
log.Fatal(err)
}
log.Printf("%+v¥n", r)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
ですが、今回はslimテンプレートエンジンでhtml文を書きたいため、slimやjadeからのインスパイヤテンプレートエンジンAce
を使って書きたいと思います。
gom 'github.com/pressly/goose/cmd/goose'
gom 'github.com/yosssi/ace'
Aceではace拡張子のHTML文を書きますが、その構造はslimに似ています。
= doctype html
html lang=ja
head
meta charset=UTF-8
title ログイン
body
h1 テスト
package main
import (
"log"
"net/http"
"github.com/yosssi/ace"
)
func handler(w http.ResponseWriter, r *http.Request) {
tmpl, err := ace.Load("templates/login", "", nil)
if err != nil {
log.Fatal(err)
}
err = tmpl.Execute(w, "")
if err != nil {
log.Fatal(err)
}
log.Printf("%+v¥n", r)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
ではとりあえず、ログイン画面とログイン成功後のTOPページを作っていこうと思います。
取り急ぎ、BootStarp4関連のリソース(CSS,JS)をCDNで読み込んで、ログインフォームを作っていきます。
コードはhttps://getbootstrap.com/docs/4.1/examples/sign-in/のサンプルコードを適当に修正をかけました。
= doctype html
html lang=ja
head
meta charset=UTF-8
meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"
link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"
link rel="stylesheet" href="https://getbootstrap.com/docs/4.1/examples/sign-in/signin.css"
title ログイン
body.text-center
form.form-signin action="/login" method="POST"
h1.h3.mb-3.font-weight-normal ログイン画面
input#input_username.form-control type="text" placeholder="ユーザー名" required="" autofocus="" autocomplete="off" name="username"
input#input_password.form-control type="password" placeholder="パスワード" required="" autocomplete="off" name="password"
button.btn.btn-lg.btn-primary.btn-block type="submit" サインイン
script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"
script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"
script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"
では、これをGET,POSTリクエストで分けられるようにイベントハンドラを作成していきます。
package main
import (
"fmt"
"log"
"net/http"
"github.com/yosssi/ace"
)
func login_handler(w http.ResponseWriter, r *http.Request) {
fmt.Println("method:", r.Method) //リクエストを取得するメソッド
// ログイン画面の取得(GET)もしくはログイン遷移の処理(POST)を行う
switch r.Method {
case "GET":
tmpl, err := ace.Load("templates/login", "", nil)
if err != nil {
log.Fatal(err)
}
err = tmpl.Execute(w, "")
if err != nil {
log.Fatal(err)
}
case "POST":
r.ParseForm()
fmt.Println("username:", r.FormValue("username"))
fmt.Println("password:", r.FormValue("password"))
}
log.Printf("%+v¥n", r)
}
func main() {
http.HandleFunc("/login", login_handler)
http.ListenAndServe(":8080", nil)
}
リクエストメソッドはr.Method
で取得できるため、これを利用してGET,POSTリクエスト時の処理を分けます。
また、r.ParseForm()
によって、URLが渡すオプションを解析します。
今回はログイン画面なため、ユーザ名とパスワードの値がそれぞれ一つずつ取れればいいため、解析後にr.FormValue
で直接フォームの値を取得します。
サンプル用のユーザを作成
% cd src/dairy_report/db/migrations/
% goose create insert_example_user
2018/10/07 23:41:16 Created new file: 20180930203150_insert_example_user.go
% mv src/dairy_report/db/migrations/20180930203150_insert_example_user.go src/dairy_report/db/migrations/20180930203150_insert_example_user.sql
-- +goose Up
INSERT INTO users VALUES
(1, 'test', 'test1234');
-- +goose Down
DELETE FROM users WHERE id = 1;
% /go/src/dairy_report/db/migrations
% ../../vendor/bin/goose postgres "host=$DAIRY_REPORT_DEVELOPMENT_HOST user=$DAIRY_REPORT_DEVELOPMENT_USERNAME password=$DAIRY_REPORT_DEVELOPMENT_PASSWORD dbname=$DAIRY_REPORT_DEVELOPMENT_DATABASE sslmode=disable" up
dairy_report_development=# select * from users;
id | username | password
----+----------+----------
1 | test | test1234
(1 row)
DBのORMマッパーを扱う
いよいよ、作成したユーザデータのパスワードを確認し、フォームに入力されたパスワードと一致すればTOPページにリダイレクトするようにします。DBからデータを取得する必要があるため、ORMマッパーを利用する必要があります。今回は、標準ライブラリであるdatabase/sql
が一番扱いやすいためこれを採用しました。
また、PostgreSQLのドライバを利用するため、github.com/lib/pq
のドライバを利用しています。
gom 'github.com/pressly/goose/cmd/goose'
gom 'github.com/yosssi/ace'
gom 'github.com/lib/pq'
package main
import (
"fmt"
"log"
"net/http"
"github.com/yosssi/ace"
"database/sql"
_ "github.com/lib/pq" //このアンダースコアを入れることでpqを直接使っているメソッドがないコードでエラーが発生しないようにしています
)
func login_handler(w http.ResponseWriter, r *http.Request) {
fmt.Println("method:", r.Method) //リクエストを取得するメソッド
// ログイン画面の取得(GET)もしくはログイン遷移の処理(POST)を行う
switch r.Method {
case "GET":
tmpl, err := ace.Load("templates/login", "", nil)
if err != nil {
log.Fatal(err)
}
err = tmpl.Execute(w, "")
if err != nil {
log.Fatal(err)
}
case "POST":
type User struct {
UserName string
Password string
}
var id int
var username string
r.ParseForm()
fmt.Println("username:", r.FormValue("username"))
fmt.Println("password:", r.FormValue("password"))
user := User{ r.FormValue("username"), r.FormValue("password")}
db, err := sql.Open("postgres", "host=db user=dairy_report password=dairy_report dbname=dairy_report_development sslmode=disable")
if err != nil {
fmt.Println(err)
}
query := `SELECT id, username FROM users where username = $1 AND password = $2`
err = db.QueryRow(query, user.UserName, user.Password).Scan(&id, &username) // データの取得に成功した場合はidとusernameに各値を格納する
if err != nil {
fmt.Println(err)
http.Redirect(w, r, "/login", http.StatusFound)
}
fmt.Println(id, username)
http.Redirect(w, r, "/", http.StatusFound)
}
log.Printf("%+v¥n", r)
}
func main() {
http.HandleFunc("/login", login_handler)
http.ListenAndServe(":8080", nil)
}
以上で、ログイン画面の実装が完了しました(TOPページはまだaceテンプレートを用意していないため、NOT Foundです)。
まとめ
長くなりましたが、ログイン画面を実装するまでの記事でした。エラー処理のリファクタリングやPOSTリクエストの二重防止、CSRFトークンの処理など、フォーム処理に必要なものをまだ入れていませんが、エビデンスを調べるのにかなり時間がかかっているため、一旦ここでストップしておきます。
次回の実装内容については考え中です。それでは。