diff --git a/03/Dockerfile b/03/Dockerfile new file mode 100644 index 0000000..a625cfa --- /dev/null +++ b/03/Dockerfile @@ -0,0 +1,3 @@ +FROM mysql:5.7 +EXPOSE 3306 +CMD ["mysqld"] diff --git a/03/Makefile b/03/Makefile new file mode 100644 index 0000000..3fec4aa --- /dev/null +++ b/03/Makefile @@ -0,0 +1,43 @@ +run: + go build -o build . && ./build + +test: + export HTTPDOC=1 && go test -v ./... + +docker-build: + docker-compose build + +docker-up: + docker-compose up -d + +docker-stop: + docker-compose stop + +docker-rm: + docker-compose rm + +docker-ssh: + docker exec -it goapi /bin/bash + +docker-server: docker-build docker-up + +docker-clean: docker-stop docker-rm + +config-set: + cp -i db.yml.tmpl db.yml + +host:=http://localhost:8080 +auth:=admin +token:=token + +curl-auth-login: + curl $(host)/api/auth/login?token=$(token) + +curl-members-id: + curl -H 'Authorization:$(auth)' $(host)/api/members/$(id) + +curl-members: + curl -H 'Authorization:$(auth)' $(host)/api/members + +curl-reqid: + curl -H 'Authorization:$(auth)' -H 'X-Request-ID:test' $(host)/api/members diff --git a/03/controller.go b/03/controller.go new file mode 100644 index 0000000..8205464 --- /dev/null +++ b/03/controller.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/go-chi/chi" +) + +// Controller ハンドラ用 +type Controller struct { +} + +// NewController コンストラクタ +func NewController() *Controller { + return &Controller{} +} + +// User user +type User struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// Show endpoint +func (c *Controller) Show(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + id, _ := strconv.Atoi(chi.URLParam(r, "id")) + res := User{ID: id, Name: fmt.Sprint("name_", id)} + return http.StatusOK, res, nil +} + +// List endpoint +func (c *Controller) List(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + users := []User{ + {1, "hoge"}, + {2, "foo"}, + {3, "bar"}, + } + return http.StatusOK, users, nil +} + +// AuthInfo 何らかの認証後にトークン発行するようなもの +type AuthInfo struct { + Authorization string `json:"authorization"` +} + +// Login endpoint +func (c *Controller) Login(w http.ResponseWriter, r *http.Request) (int, interface{}, error) { + token := r.URL.Query().Get("token") + if token != "token" { + return http.StatusUnauthorized, nil, fmt.Errorf("有効でないトークンです: %s", token) + } + res := AuthInfo{Authorization: "admin"} + return http.StatusOK, res, nil +} diff --git a/03/db.go b/03/db.go new file mode 100644 index 0000000..7e774e6 --- /dev/null +++ b/03/db.go @@ -0,0 +1,63 @@ +package main + +import ( + "io" + "io/ioutil" + "os" + + "github.com/jmoiron/sqlx" + "gopkg.in/yaml.v1" + // MySQL driver + _ "github.com/go-sql-driver/mysql" +) + +// Configs 環境ごとの設定情報をもつ +type Configs map[string]*Config + +// Open 指定された環境についてDBに接続します。 +func (cs Configs) Open(env string) (*sqlx.DB, error) { + config, ok := cs[env] + if !ok { + return nil, nil + } + return config.Open() +} + +// Config sql-migrateの設定ファイルと同じ形式を想定している +type Config struct { + Datasource string `yaml:"datasource"` +} + +// DSN 設定されているDSNを返します +func (c *Config) DSN() string { + return c.Datasource +} + +// Open Configで指定されている接続先に接続する。 +// MySQL固定 +func (c *Config) Open() (*sqlx.DB, error) { + return sqlx.Open("mysql", c.DSN()) +} + +// NewConfigsFromFile Configから設定を読み取る +func NewConfigsFromFile(path string) (Configs, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() // nolint: errcheck + return NewConfigs(f) +} + +// NewConfigs io.ReaderからDB用設定を読み取る +func NewConfigs(r io.Reader) (Configs, error) { + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + var configs Configs + if err = yaml.Unmarshal(b, &configs); err != nil { + return nil, err + } + return configs, nil +} diff --git a/03/db.yml.tmpl b/03/db.yml.tmpl new file mode 100644 index 0000000..e910d76 --- /dev/null +++ b/03/db.yml.tmpl @@ -0,0 +1,25 @@ +# Example Database Configurationgorp +# +# For using gorp, enable parseTime option on MySQL to serialize/deserialize time.Time. +# +# see: https://github.com/rubenv/sql-migrate/issues/2 +# +# Also interpolateParams=true, to replace placement on database server. +# +# see: https://github.com/go-sql-driver/mysql/pull/309 +# see: http://dsas.blog.klab.org/archives/52191467.html +develop: + dialect: mysql + datasource: go-chi:password@tcp(localhost:3306)/go-chi?parseTime=true&collation=utf8mb4_general_ci&interpolateParams=true&loc=UTC + dir: mysql/migrations + +staging: + dialect: mysql + datasource: go-chi:password@tcp(localhost:3306)/go-chi?parseTime=true&collation=utf8mb4_general_ci&interpolateParams=true&loc=UTC + dir: mysql/migrations + +production: + dialect: mysql + datasource: go-chi:password@tcp(localhost:3306)/go-chi?parseTime=true&collation=utf8mb4_general_ci&interpolateParams=true&loc=UTC + dir: mysql/migrations + diff --git a/03/db_test.go b/03/db_test.go new file mode 100644 index 0000000..33301e6 --- /dev/null +++ b/03/db_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadConfig(t *testing.T) { + assert := assert.New(t) + r := strings.NewReader(` +development: + datasource: root@localhost/dev + +test: + datasource: root@localhost/test +`) + + configs, err := NewConfigs(r) + assert.NoError(err) + c, ok := configs["development"] + assert.True(ok) + assert.Equal("root@localhost/dev", c.DSN(), "they should be equal") +} diff --git a/03/doc/doc.md b/03/doc/doc.md new file mode 100644 index 0000000..eab2db7 --- /dev/null +++ b/03/doc/doc.md @@ -0,0 +1,54 @@ +# github.com/pei0804/go-chi-api-example + +generated docs. + +## Routes + +
+`/api/*/members/*` + +- [github.com/go-chi/cors.(*Cors).Handler-fm](/03/main.go#L49) +- [RequestIDHandler](https://github.com/ascarter/requestid/requestid.go#L28) +- [CloseNotify](https://github.com/go-chi/chi/middleware/closenotify18.go#L16) +- [main.loggingMiddleware](/03/middleware.go#L28) +- [Timeout.func1](https://github.com/go-chi/chi/middleware/timeout.go#L33) +- **/api/*** + - [main.Auth.func1](/03/middleware.go#L15) + - **/members/*** + - **/** + - _GET_ + - [main.(handler).ServeHTTP-fm](/03/main.go#L62) + +
+
+`/api/*/members/*/{id}` + +- [github.com/go-chi/cors.(*Cors).Handler-fm](/03/main.go#L49) +- [RequestIDHandler](https://github.com/ascarter/requestid/requestid.go#L28) +- [CloseNotify](https://github.com/go-chi/chi/middleware/closenotify18.go#L16) +- [main.loggingMiddleware](/03/middleware.go#L28) +- [Timeout.func1](https://github.com/go-chi/chi/middleware/timeout.go#L33) +- **/api/*** + - [main.Auth.func1](/03/middleware.go#L15) + - **/members/*** + - **/{id}** + - _GET_ + - [main.(handler).ServeHTTP-fm](/03/main.go#L62) + +
+
+`/api/auth/*/login` + +- [github.com/go-chi/cors.(*Cors).Handler-fm](/03/main.go#L49) +- [RequestIDHandler](https://github.com/ascarter/requestid/requestid.go#L28) +- [CloseNotify](https://github.com/go-chi/chi/middleware/closenotify18.go#L16) +- [main.loggingMiddleware](/03/middleware.go#L28) +- [Timeout.func1](https://github.com/go-chi/chi/middleware/timeout.go#L33) +- **/api/auth/*** + - **/login** + - _GET_ + - [main.(handler).ServeHTTP-fm](/03/main.go#L62) + +
+ +Total # of routes: 3 diff --git a/03/doc/list.md b/03/doc/list.md new file mode 100644 index 0000000..30db407 --- /dev/null +++ b/03/doc/list.md @@ -0,0 +1,69 @@ +# API doc + +This is API documentation for List Controller. This is generated by `httpdoc`. Don't edit by hand. + +## Table of contents + +- [[200] GET /api/members](#200-get-apimembers) + + +## [200] GET /api/members + +get user list + +### Request + + + +Headers + +| Name | Value | Description | +| ----- | :----- | :--------- | +| Accept-Encoding | gzip | | +| Authorization | admin | auth token | +| User-Agent | Go-http-client/1.1 | | + + + + + + + +### Response + +Headers + +| Name | Value | Description | +| ----- | :----- | :--------- | +| Content-Type | application/json | | + + + + + +Response example + +
+Click to expand code. + +```javascript +[ + { + "id": 1, + "name": "hoge" + }, + { + "id": 2, + "name": "foo" + }, + { + "id": 3, + "name": "bar" + } +] +``` + +
+ + + diff --git a/03/doc/login.md b/03/doc/login.md new file mode 100644 index 0000000..afd3a66 --- /dev/null +++ b/03/doc/login.md @@ -0,0 +1,68 @@ +# API doc + +This is API documentation for Login Controller. This is generated by `httpdoc`. Don't edit by hand. + +## Table of contents + +- [[200] GET /api/auth/login](#200-get-apiauthlogin) + + +## [200] GET /api/auth/login + +get user show + +### Request + +Parameters + +| Name | Value | Description | +| ----- | :----- | :--------- | +| token | token | | + + +Headers + +| Name | Value | Description | +| ----- | :----- | :--------- | +| Accept-Encoding | gzip | | +| User-Agent | Go-http-client/1.1 | | + + + + + + + +### Response + +Headers + +| Name | Value | Description | +| ----- | :----- | :--------- | +| Content-Type | application/json | | + + + +Response fields + +| Name | Value | Description | +| ----- | :----- | :--------- | +| Authorization | admin | | + + + +Response example + +
+Click to expand code. + +```javascript +{ + "authorization": "admin" +} +``` + +
+ + + diff --git a/03/doc/show.md b/03/doc/show.md new file mode 100644 index 0000000..04bd2d8 --- /dev/null +++ b/03/doc/show.md @@ -0,0 +1,66 @@ +# API doc + +This is API documentation for Show Controller. This is generated by `httpdoc`. Don't edit by hand. + +## Table of contents + +- [[200] GET /api/members/1](#200-get-apimembers1) + + +## [200] GET /api/members/1 + +get user show + +### Request + + + +Headers + +| Name | Value | Description | +| ----- | :----- | :--------- | +| Accept-Encoding | gzip | | +| Authorization | admin | auth token | +| User-Agent | Go-http-client/1.1 | | + + + + + + + +### Response + +Headers + +| Name | Value | Description | +| ----- | :----- | :--------- | +| Content-Type | application/json | | + + + +Response fields + +| Name | Value | Description | +| ----- | :----- | :--------- | +| ID | 1 | | +| Name | name_1 | | + + + +Response example + +
+Click to expand code. + +```javascript +{ + "id": 1, + "name": "name_1" +} +``` + +
+ + + diff --git a/03/docker-compose.yml b/03/docker-compose.yml new file mode 100644 index 0000000..de14761 --- /dev/null +++ b/03/docker-compose.yml @@ -0,0 +1,11 @@ +db: + build: . + container_name: go-chi + command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: go-chi + MYSQL_USER: go-chi + MYSQL_PASSWORD: password + ports: + - "3306:3306" diff --git a/03/error.go b/03/error.go new file mode 100644 index 0000000..b24240b --- /dev/null +++ b/03/error.go @@ -0,0 +1,13 @@ +package main + +import "fmt" + +// HTTPError エラー用 +type HTTPError struct { + Code int + Message string +} + +func (he *HTTPError) Error() string { + return fmt.Sprintf("code=%d, message=%v", he.Code, he.Message) +} diff --git a/03/handler.go b/03/handler.go new file mode 100644 index 0000000..0943f23 --- /dev/null +++ b/03/handler.go @@ -0,0 +1,59 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "runtime/debug" +) + +var logger = MustGetLogger("api") + +type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error) + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + if rv := recover(); rv != nil { + log.Print(rv) + debug.PrintStack() + logger.Errorf("panic: %s", rv) + http.Error(w, http.StatusText( + http.StatusInternalServerError), http.StatusInternalServerError) + } + }() + status, res, err := h(w, r) + if err != nil { + logger.Infof("error: %s", err) + respondError(w, status, err) + return + } + respondJSON(w, status, res) + return +} + +// respondJSON レスポンスとして返すjsonを生成して、writerに書き込む +func respondJSON(w http.ResponseWriter, status int, payload interface{}) { + response, err := json.MarshalIndent(payload, "", " ") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write([]byte(response)) +} + +// respondError レスポンスとして返すエラーを生成する +func respondError(w http.ResponseWriter, code int, err error) { + log.Printf("err: %v", err) + if e, ok := err.(*HTTPError); ok { + respondJSON(w, e.Code, e) + } else if err != nil { + he := HTTPError{ + Code: code, + Message: err.Error(), + } + respondJSON(w, code, he) + } +} diff --git a/03/logging.go b/03/logging.go new file mode 100644 index 0000000..501ed95 --- /dev/null +++ b/03/logging.go @@ -0,0 +1,124 @@ +package main + +import ( + "io" + "io/ioutil" + "log" + "os" + + logging "github.com/op/go-logging" +) + +const ( + defaultLogFormat = "[%{module}:%{level}] %{message}" +) + +// Logger wraps op/go-logging.Logger +type Logger struct { + *logging.Logger +} + +// Level embedes the logging's level +type Level int + +// Log levels. +const ( + CRITICAL Level = iota + ERROR + WARNING + NOTICE + INFO + DEBUG +) + +var levelNames = []string{ + "CRITICAL", + "ERROR", + "WARNING", + "NOTICE", + "INFO", + "DEBUG", +} + +// LogConfig logger configurations +type LogConfig struct { + // for internal usage + level Level + // Level convertes to level during initialization + Level string + // list of all modules + Modules []string + // format + Format string + // enable colors + Colors bool + // output + Output io.Writer +} + +// LogLevel parse the log level string +func LogLevel(level string) (logging.Level, error) { + return logging.LogLevel(level) +} + +// TODO: +// DefaultLogConfig vs (DevLogConfig + ProdLogConfig) ? + +// DevLogConfig default development config for logging +func DevLogConfig(modules []string) *LogConfig { + return &LogConfig{ + level: DEBUG, // int + Level: "debug", // string + Modules: modules, + Format: defaultLogFormat, + Colors: true, + Output: os.Stdout, + } +} + +// ProdLogConfig Default production config for logging +func ProdLogConfig(modules []string) *LogConfig { + return &LogConfig{ + level: ERROR, + Level: "error", + Modules: modules, + Format: defaultLogFormat, + Colors: false, + Output: os.Stdout, + } +} + +// convertes l.Level (string) to l.level (int) +// or panics if l.Level is invalid +func (l *LogConfig) initLevel() { + level, err := logging.LogLevel(l.Level) + if err != nil { + log.Panicf("Invalid -log-level %s: %v", l.Level, err) + } + l.level = Level(level) +} + +// InitLogger initialize logging using this LogConfig; +// it panics if l.Format is invalid or l.Level is invalid +func (l *LogConfig) InitLogger() { + l.initLevel() + + format := logging.MustStringFormatter(l.Format) + logging.SetFormatter(format) + for _, s := range l.Modules { + logging.SetLevel(logging.Level(l.level), s) + } + stdout := logging.NewLogBackend(l.Output, "", 0) + stdout.Color = l.Colors + logging.SetBackend(stdout) +} + +// MustGetLogger safe initialize global logger +func MustGetLogger(module string) *Logger { + return &Logger{logging.MustGetLogger(module)} +} + +// Disable disables the logger completely +func Disable() { + logging.SetBackend(logging.NewLogBackend(ioutil.Discard, "", 0)) +} diff --git a/03/main.go b/03/main.go new file mode 100644 index 0000000..4751ab9 --- /dev/null +++ b/03/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/ascarter/requestid" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/docgen" + "github.com/jmoiron/sqlx" +) + +// Server Server +type Server struct { + router *chi.Mux + db *sqlx.DB +} + +// New Server構造体のコンストラクタ +func New() *Server { + return &Server{ + router: chi.NewRouter(), + } +} + +// Init 実行時にしたいこと +func (s *Server) Init(env string) { + cs, err := NewConfigsFromFile(filepath.Join("db.yml")) + if err != nil { + log.Fatalf("cannot open database configuration. please, $make config-set. faild: %s", err) + } + s.db, err = cs.Open(env) + if err != nil { + log.Fatalf("database initialization failed: %s", err) + } + if s.db.Ping() != nil { + log.Fatalf("database ping failed: %s", s.db.Ping()) + } +} + +// Middleware ミドルウェア +func (s *Server) Middleware(env string) { + s.router.Use(CorsConfig[env].Handler) + s.router.Use(requestid.RequestIDHandler) + s.router.Use(middleware.CloseNotify) + s.router.Use(loggingMiddleware) + s.router.Use(middleware.Timeout(time.Second * 60)) +} + +// Router ルーティング設定 +func (s *Server) Router() { + c := NewController() + s.router.Route("/api", func(api chi.Router) { + api.Use(Auth("db connection")) + api.Route("/members", func(members chi.Router) { + members.Get("/{id}", handler(c.Show).ServeHTTP) + members.Get("/", handler(c.List).ServeHTTP) + }) + }) + s.router.Route("/api/auth", func(auth chi.Router) { + auth.Get("/login", handler(c.Login).ServeHTTP) + }) +} + +func main() { + var ( + port = flag.String("port", "8080", "addr to bind") + env = flag.String("env", "develop", "実行環境 (production, staging, develop)") + gendoc = flag.Bool("gendoc", true, "ドキュメント自動生成") + ) + flag.Parse() + s := New() + s.Init(*env) + s.Middleware(*env) + s.Router() + logcfg := DevLogConfig([]string{"api"}) + logcfg.Format = "%{time} [%{module}:%{level:.4s}]%{message}[%{shortfile}]" + logcfg.Colors = true + logcfg.InitLogger() + logger := MustGetLogger("api") + logger.Error("aaa") + logger.Info("a") + if *gendoc { + doc := docgen.MarkdownRoutesDoc(s.router, docgen.MarkdownOpts{ + ProjectPath: "github.com/pei0804/go-chi-api-example", + Intro: "generated docs.", + }) + file, err := os.Create("doc/doc.md") + if err != nil { + log.Printf("err: %v", err) + } + defer file.Close() + file.Write(([]byte)(doc)) + } + http.ListenAndServe(fmt.Sprint(":", *port), s.router) +} diff --git a/03/main_test.go b/03/main_test.go new file mode 100644 index 0000000..9ea33d3 --- /dev/null +++ b/03/main_test.go @@ -0,0 +1,149 @@ +package main + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi" + httpdoc "go.mercari.io/go-httpdoc" +) + +func TestListController(t *testing.T) { + document := &httpdoc.Document{ + Name: "List Controller", + } + defer func() { + if err := document.Generate("doc/list.md"); err != nil { + t.Fatalf("err: %s", err) + } + }() + + // mux := http.NewServeMux() + router := chi.NewRouter() + c := NewController() + router.Method("GET", "/api/members", httpdoc.Record(handler(c.List), document, &httpdoc.RecordOption{ + Description: "get user list", + WithValidate: func(validator *httpdoc.Validator) { + validator.RequestParams(t, []httpdoc.TestCase{}) + validator.RequestHeaders(t, []httpdoc.TestCase{ + {Target: "Authorization", Expected: "admin", Description: "auth token"}, + }) + validator.ResponseStatusCode(t, http.StatusOK) + // FIXME slice validation + // validator.ResponseBody(t, []httpdoc.TestCase{ + // {Target: "id", Expected: 1, Description: ""}, + // {Target: "name", Expected: "hoge", Description: ""}, + // }, &[]User{}) + }, + })) + + testServer := httptest.NewServer(router) + defer testServer.Close() + req, err := http.NewRequest(http.MethodGet, testServer.URL+"/api/members", nil) + req.Header.Add("Authorization", "admin") + if err != nil { + t.Fatalf("err: %s", err) + } + resp, err := http.DefaultClient.Do(req) + + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("err: %s", err) + } + jsonStr := `[ + { + "id": 1, + "name": "hoge" + }, + { + "id": 2, + "name": "foo" + }, + { + "id": 3, + "name": "bar" + } +]` + if string(b) != jsonStr { + t.Fatalf("err: %s", err) + } +} + +func TestShowController(t *testing.T) { + document := &httpdoc.Document{ + Name: "Show Controller", + } + defer func() { + if err := document.Generate("doc/show.md"); err != nil { + t.Fatalf("err: %s", err) + } + }() + // mux := mux.NewRouter() + c := NewController() + router := chi.NewRouter() + router.Method("GET", "/api/members/{id}", httpdoc.Record(handler(c.Show), document, &httpdoc.RecordOption{ + Description: "get user show", + WithValidate: func(validator *httpdoc.Validator) { + validator.RequestHeaders(t, []httpdoc.TestCase{ + {Target: "Authorization", Expected: "admin", Description: "auth token"}, + }) + validator.ResponseStatusCode(t, http.StatusOK) + validator.ResponseBody(t, []httpdoc.TestCase{ + {Target: "ID", Expected: 1, Description: "user id"}, + {Target: "Name", Expected: "name_1", Description: "user name"}}, + &User{}, + ) + }, + })) + testServer := httptest.NewServer(router) + defer testServer.Close() + req, err := http.NewRequest(http.MethodGet, testServer.URL+"/api/members/1", nil) + req.Header.Add("Authorization", "admin") + if err != nil { + t.Fatalf("err: %s", err) + } + _, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestLoginController(t *testing.T) { + document := &httpdoc.Document{ + Name: "Login Controller", + } + defer func() { + if err := document.Generate("doc/login.md"); err != nil { + t.Fatalf("err: %s", err) + } + }() + // mux := mux.NewRouter() + c := NewController() + router := chi.NewRouter() + router.Method("GET", "/api/auth/login", httpdoc.Record(handler(c.Login), document, &httpdoc.RecordOption{ + Description: "auth login", + WithValidate: func(validator *httpdoc.Validator) { + validator.RequestParams(t, []httpdoc.TestCase{ + {Target: "token", Expected: "token", Description: "token"}, + }) + validator.ResponseStatusCode(t, http.StatusOK) + validator.ResponseBody(t, []httpdoc.TestCase{ + {Target: "Authorization", Expected: "admin", Description: "Authorization info"}}, + &AuthInfo{}, + ) + }, + })) + testServer := httptest.NewServer(router) + defer testServer.Close() + req, err := http.NewRequest(http.MethodGet, testServer.URL+"/api/auth/login?token=token", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + _, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/03/middleware.go b/03/middleware.go new file mode 100644 index 0000000..ba8cd6b --- /dev/null +++ b/03/middleware.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/ascarter/requestid" + "github.com/go-chi/cors" + "github.com/google/uuid" +) + +// Auth 認証(dbはフェイク) +func Auth(db string) (fn func(http.Handler) http.Handler) { + fn = func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token != "admin" { + respondError(w, http.StatusUnauthorized, fmt.Errorf("利用権限がありません")) + return + } + h.ServeHTTP(w, r) + }) + } + return +} + +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t1 := time.Now() + next.ServeHTTP(w, r) + t2 := time.Now() + t := t2.Sub(t1) + reqID, ok := requestid.FromContext(r.Context()) + if !ok { + reqID = uuid.New().String() + } + logger.Infof("request_id %s req_time %s req_time_nsec %v", reqID, t.String(), t.Nanoseconds()) + }) +} + +var devCORS = cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, +}) + +var stagingCORS = cors.New(cors.Options{ + AllowedOrigins: []string{"staging.com"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, +}) + +var productionCORS = cors.New(cors.Options{ + AllowedOrigins: []string{"production.com"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, +}) + +// CorsConfig CORSの設定を環境別に持っている +var CorsConfig = map[string]*cors.Cors{ + "develop": devCORS, + "staging": stagingCORS, + "production": productionCORS, +} diff --git a/03/model.go b/03/model.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/03/model.go @@ -0,0 +1 @@ +package main diff --git a/03/runner.conf b/03/runner.conf new file mode 100644 index 0000000..0d4364a --- /dev/null +++ b/03/runner.conf @@ -0,0 +1,14 @@ +root: . +tmp_path: ./tmp +build_name: runner-build +build_log: runner-build-errors.log +valid_ext: .go, .tpl, .tmpl, .html +no_rebuild_ext: .tpl, .tmpl, .html +ignored: assets, tmp +build_delay: 600 +colors: 1 +log_color_main: cyan +log_color_build: yellow +log_color_runner: green +log_color_watcher: magenta +log_color_app: