ginのログ出力先をsyslogにする
こんなかんじ
package main import "github.com/gin-gonic/gin" import "log/syslog" func main() { logger, err := syslog.New( syslog.LOG_NOTICE|syslog.LOG_USER, "gin-test") if err != nil { panic(err) } gin.SetMode(gin.ReleaseMode) gin.DisableConsoleColor() gin.DefaultWriter = logger app := gin.Default() grp := app.Group("/api") grp.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "ok"}) }) app.Run("localhost:3000") }
まぁでもsupervisorとか使うだろうからログも標準出力にだして、あとはsupervisorに任せればよいでしょう。(※と、当初書いたけれど、systemdでやればいいんじゃないのって言われたが、とりあえず記事はそのまま残しておく。)
RHEL8系の場合、
sudo dnf install epel-release
ってやって(すでにやってあれば当然不要)
sudo yum -y install supervisor
とかやるとインストールされ、設定ファイルは /etc/supervisord.conf になっているので、そのなかに設定を追記するなり、設定ファイルをよく見るとわかるように /etc/supervisord.d/*.iniが読み込まれるようになっているのでそれにマッチする個別の設定ファイルを置くなりして自分のプログラムがsupervisordに起動されるようにすればよい。
例えば自分のプログラムがapiという名前で識別されるようにしたかったら、/etc/supervisord.d/api.ini などといった名前のファイルを作って、その中に
[program:api] command=/usr/local/bin/api autostart=true autorestart=true stderr_logfile=/var/log/api.err.log stderr_logfile=/var/log/api.out.log
などと書いてやればよいだろう。
ってsupervisorの話のほうが長くなった。
firewalldの設定を確認する方法
firewall-cmd --list-all
それだけ。
ginでAPIサーバを作る
ディレクトリをほって初期化する。 モジュール名のところはリポジトリ名にするんだよ、とする情報が多くて、正しいのだけれど、公開しないものであれば別にそのフォルダ名とかでも特に支障はない(たぶん)。
mkdir XXXX cd XXXX go mod init モジュール名
DBを立ち上げるdocker-compose.ymlをかく
services: db: image: postgres:latest container_name: postgresql ports: - 5432:5432 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres volumes: - ./pgdata:/var/lib/postgresql/data - ./pginit:/docker-entrypoint-initdb.d
初期化して初期データをつっこむSQLを書く
DROP TABLE IF EXISTS Users; CREATE TABLE Users ( user_id SERIAL, username VARCHAR(128), account VARCHAR(64), password VARCHAR(64) ); INSERT INTO Users(username, account, password) VALUES('あ', 'user01', 'user01'); INSERT INTO Users(username, account, password) VALUES('い', 'user02', 'user02'); INSERT INTO Users(username, account, password) VALUES('う', 'user03', 'user03'); INSERT INTO Users(username, account, password) VALUES('え', 'user04', 'user04'); INSERT INTO Users(username, account, password) VALUES('お', 'user05', 'user05');
勢いで書く。
package main import ( "database/sql" "fmt" "log" "strconv" "strings" "time" _ "github.com/lib/pq" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" ) const SECRETKEY = "secretkey" func main() { // ログにファイル名と行番号を出す設定 log.SetFlags(log.Lshortfile) app := gin.Default() // グループの作成 apiRouter := app.Group("/api/v0") // CORS設定、どこからでもアクセスOKにしてみた apiRouter.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"POST", "GET", "OPTIONS"}, AllowHeaders: []string{"Authorization", "Content-Type"}})) // このグループ共通のミドル、ログインしていたらユーザ情報を取得する apiRouter.Use(middleware) // ログイン apiRouter.POST("/login", Login) // ログインしているユーザの情報の取得 apiRouter.GET("/me", Me) app.Run() } // // Authorizationヘッダに正しいトークンが乗っていたらコンテキストにそのユーザの情報を乗せる // func middleware(c *gin.Context) { // Authorizationヘッダからトークンを取り出す authorizationHeader := c.Request.Header.Get("Authorization") if authorizationHeader != "" { ary := strings.Split(authorizationHeader, " ") if len(ary) == 2 { if ary[0] == "Bearer" { // ここまででトークンと思われるものを取り出せたので、解析する // ここのロジックは公式サイト参照 // https://pkg.go.dev/github.com/golang-jwt/jwt#example-Parse-Hmac token, err := jwt.Parse(ary[1], func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } return []byte(SECRETKEY), nil }) if err == nil { // 解析に成功したら、ユーザIDを取り出す if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { userIDStr := claims["sub"].(string) userID, err := strconv.Atoi(userIDStr) if err == nil { // ユーザIDを使って新しいトークンを取り出す(有効期限切れ対策) newToken, err := generateToken(userID) if err == nil { // ユーザの情報を取り出す conn, err := connectDB() if err == nil { defer conn.Close() var username string err := conn.QueryRow(` SELECT username FROM Users WHERE user_id = $1`, userID).Scan(&username) if err == nil { // 新しいトークンとユーザの情報をコンテキストに保存 c.Set("token", newToken) c.Set("username", username) } } } } } } } } } c.Next() } // // 投入されたログイン情報を格納するための構造体 // type LoginInfo struct { Account string `json:"account" binding:"required"` Password string `json:"password" binding:"required"` } // // ログイン処理 // 正しいアカウントとパスワードが渡されたらトークンを生成して返す // func Login(c *gin.Context) { // 投げつけられた情報を取り出す var loginInfo LoginInfo err := c.ShouldBindJSON(&loginInfo) if err != nil { log.Println(err) c.JSON(400, gin.H{"message": "bad request"}) return } // DBに接続 conn, err := connectDB() if err != nil { log.Println(err) c.JSON(500, gin.H{"message": "server internal error"}) return } defer conn.Close() // 渡された情報に該当するユーザを探す。 // 本当だったらDBに生パスワードは保存しないだろうからこの前にハッシュ化などが必要 var userID int rows, err := conn.Query(` SELECT user_id FROM Users WHERE account = $1 AND password = $2`, loginInfo.Account, loginInfo.Password) for rows.Next() { err := rows.Scan(&userID) if err != nil { log.Println(err) c.JSON(500, gin.H{"message": "server internal error"}) return } } // トークン生成 token, err := generateToken(userID) if err != nil { c.JSON(500, gin.H{"message": "server error"}) return } c.JSON(200, gin.H{"token": token}) } // // ログインしているユーザの自分自身の情報取得 // func Me(c *gin.Context) { // 自作ミドルウェアがコンテキストに保存した情報を取り出す username, exist := c.Get("username") if !exist { c.JSON(403, gin.H{"message": "need to login"}) return } token, exist := c.Get("token") if !exist { c.JSON(403, gin.H{"message": "need to login"}) return } c.JSON(200, gin.H{"username": username, "token": token}) } // // DBへの接続 // func connectDB() (*sql.DB, error) { db, err := sql.Open("postgres", ` host=localhost port=5432 dbname=postgres user=postgres password=postgres sslmode=disable`) if err != nil { log.Println(err) return nil, err } return db, nil } // // トークン生成 // func generateToken(userID int) (string, error) { now := time.Now() token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "sub": fmt.Sprintf("%d", userID), "iat": now.Unix(), "exp": now.Add(60 * time.Minute).Unix(), // 一時間で無効になる }) return token.SignedString([]byte(SECRETKEY)) }
TypeScriptでexpressを使ってAPIサーバを作る
初期設定
とりえあえず、TypeScriptとexpressとexpressの型定義をインストールする。
npm i express npm i -D typescript npm i -D @types/express
はじめの一歩
expressを用いたWebアプリでは、expressのオブジェクトを作り、そのオブジェクトにどのメソッドでどのURLに対してどんなリクエストが来たらどんなレスポンスを返すのか、をひたすら実装していく。
import express from "express"; const app = express(); const port: number = 3000; app.get("/", (Req: express.Request, res: express.Response) => { res.json({ message: "ok" }); // res.status(500).json({ message: "error"}) // エラーレスポンスはこんな感じ }); app.listen(port, () => console.log("ok, port =", port));
パスパラメータ
パスを指定する場所で、たとえば:user_idのように、コロンに続けて変数名を指定する。例として、ユーザIDを受け取るような状況を考える。
import express from "express"; const app = express(); const port: number = 3000; app.get("/:user_id", (req: express.Request, res: express.Response) => { res.json({ message: "ok", user_id: req.params.user_id }); }); app.listen(port, () => console.log("ok, port =", port));
このように、req.paramsに続けて受け取った変数名を書くことで、パスを用いて渡された変数の値を取得することができるので、例えば~/1などにアクセスすることができるようになる。
POSTされたJSONの受け取り方
const port: number = 3000; import express from "express"; const app = express(); import bodyParser from "body-parser"; app.use( bodyParser.urlencoded({ extended: true, }) ); app.use(bodyParser.json()); app.post("/:user_id", (req: express.Request, res: express.Response) => { res.json({ message: "ok", user_id: req.params.user_id, data: req.body }); }); app.listen(port, () => console.log("ok, port =", port));
ルート
ルートを使うとあるパス配下の処理をまとめて扱うことができる。またルート単位に処理を割り当てて、そのルート配下であればリクエストがくるたびに割り当てた処理を実行させることができる。 例えば、/api/v0云々というパスに対するリクエストに来たら同じ処理をさせる場合は、以下のようなかたちになる。
import express from "express"; const app = express(); const port: number = 3000; const apiRouter = express.Router(); // ルータを作る app.use("/api/v0", apiRouter); // ルータをパスに割り当てる apiRouter.use( // このルータで使うミドルウェアを設定する。 async ( req: express.Request, res: express.Response, next: express.NextFunction ) => { // 例えばログインしている場合はユーザ情報を取得するとかその手のことをやる。 next(); // これを呼ぶと次の関数が実行される、呼ばないと実行されない。 } ); apiRouter.get("/", (req: express.Request, res: express.Response) => { res.json({ message: "ok" }); }); app.listen(port, () => console.log("ok, port =", port));
なお、このように割り当てる処理のことをミドルウェアと呼ぶ。
JWT
APIサーバでの認証方法のひとつとしてトークンを用いるという方式があり、トークンの仕様のひとつとしてIETFによりRFC7519で定められたJWTがある。
https://datatracker.ietf.org/doc/html/rfc7519
これを使えばどうにかできるだろう。
https://github.com/auth0/node-jsonwebtoken
CORS
APIサーバなので、別のサーバが配信したHTMLからたたかれることを想定しなくてはならないこともあるだろう。
import express from "express"; import cors from "cors"; const app = express(); app.use(cors());
入力値のバリデーション
express-validatorを使えばよさそう。
https://express-validator.github.io/docs/
しめのあいさつ
なにか書き足したほうがよさそうなことを思いついたら順次追記していきます。
TypeScriptの基本
TypeScriptとは
JavaScriptの世界に静的型付けを持ち込んだイメージ。JavaScriptのゆるふわ感を緩和してくれる。TypeScriptでかいたソースコードをコンパイラ(トランスパイラ)で変換するとJavaScriptのコードが生成されるので、そのソースコードをnode.jsなどで実行する。なお、Denoを使うとTypeScriptのソースを直接実行することができる。
以下、node.jsで触ってみる。
準備
適当なディレクトリを作ってからそのディレクトリ内に移動して、初期化。
npm init -y
-yをつけないとインタラクティブにいろいろ聞かれるが、-yをつけるとすべてデフォルト値で一気に設定ファイル(package.json)を生成してくれる。
TypeScriptのインストール
このプロジェクトにTypeScriptをインストールする。
npmでのパッケージのインストールには、
npm install パッケージ名
と入力する。installはiと一文字だけ入力することでも同じ結果になる。つまりこんな感じ。
npm i パッケージ名
また開発専用のパッケージをインストールする場合は、オプションで--save-devあるいは-Dオプションを付与する。-Dのほうを使うなら、こんな感じ。
npm i -D パッケージ名
TypeScriptは開発中しか使わないので、このオプションを付与してインストールする。
npm i -D typescript
これにより、./node_modules/.bin/tscという実行ファイルがインストールされる。たぶんTypeScript Compilerの略。
TypeScriptの設定ファイルを作る
TypeScriptの設定値はやまほどあるが、以下のコマンドで丁寧な説明がコメントで記載されている設定ファイルが生成されるので、これを編集すればよい。
./node_modules/.bin/tsc --init
コマンドを実行したディレクトリに設定ファイルであるtsconfig.jsonが生成されているはず。
例えば、こんな行がある。
// "outDir": "./", /* Specify an output folder for all emitted files. */
コメントに、出力先のフォルダを指定するんだよ的なことが書いてあるので、コメントアウトを外してからJavaScriptのコードを出力するフォルダ名を書く。
// "outDir": "./dist",
型定義の書き方
こんな感じ。変数名のうしろにコロンを書いて、さらにその後ろに型の名前を書く。
let n: number; let s: string; let a1: number[]; // 配列 let t: [number, string, string]; // これはタプル
オブジェクトの場合、オブジェクトリテラルを使う。
let c: {d: number} ={ d: 1234; }
オブジェクトリテラルにおいて、読み取り専用の変数にはreadonly、あってもなくてもよい変数には?を付与する。
let e: { readonly f: string; g?: number; }
型エイリアスを使うとオブジェクトを生成する都度オブジェクトリテラルを書く必要がなくなり簡潔なコードにすることができる。
type A = { h: string; i: number; }
関数での型指定
関数の引数と戻り値ならこう
function sonomamakaesu(a : number) : number { return a; }
型定義同様、?を使って省略できることを示すことができる。
function f(a: number, b?: number) { console.log(a); console.log(b); // 省略された場合はundefinedと出力される }
値を書くことで省略時のデフォルト値を指定することができる。
function f(s = "default parameter") { console.log(s); }
レストパラメータを使えば可変長引数も実現できる。
function f(...nums: number[]) { nums.map((x: number) => { console.log(x); }); }
関数自身がどんな型なのかを指定することもできる。
function f0(a: number): number { return a * a; } function f1(f: (x: number) => number): void { // ここの引数に注目 console.log(f(10)); } f1(f0);
コーディング時に型を特定できない場合はジェネリック型を使うことができる。 始まりのカッコの直前に<と>でくくって型を指し示す文字列を書いておいて、関数定義内ではその文字列を型を記すべき箇所に書いていく。
function f<T>(x: T) { console.log(x); } f(1); // 省略することもできるし、 f<string>("asdf"); // 明示することもできる。
はじめてのJSX
ReactではHTMLを組み立てる上でJSXというJavaScriptの拡張構文を用いる。
関数コンポーネントを使う場合、そのコンポーネントを表す関数でJSXをreturnする。
例えばこんな感じ
export const Abc = () => { return <p>abac</p>; }
JSXのなかでは{と}でくくることでJavaScriptの式を書くことができる。
条件分岐は3項演算子を使う。
import { useState } from "react"; export const Bbs = () => { const [flag] = useState<boolean>(true); return <div>{flag ? <>真</> : <>偽</>}</div>; };
ループにはmapを使う。returnを忘れないように。
return (<ul> { ary.map((item: string, key: number)=>{ return <li key={key}>{ item }</li> });}</ul>);