├── lp.png
├── README.md
├── go.mod
├── go.sum
├── model
├── user.go
├── comment.go
└── post.go
├── Dockerfile
├── docker-compose.yml
├── .gitignore
├── template
├── _header.html
├── users-new.html
├── posts.html
├── posts-new.html
├── post-detail.html
└── home.html
├── main.go
├── router
├── signout.go
├── home.go
├── user.go
└── posts.go
├── LICENSE
├── .github
└── workflows
│ └── deploy.yaml
└── db
└── sql
└── 1_create-table.sql
/lp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sadnessOjisan/gochann/HEAD/lp.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # learn-go-server
2 |
3 |
4 |
5 | Go でサーバー立てる練習
6 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module learn-go-server
2 |
3 | go 1.19
4 |
5 | require github.com/go-sql-driver/mysql v1.7.1
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
2 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
3 |
--------------------------------------------------------------------------------
/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | type User struct {
6 | ID int `db:"id"`
7 | Name string `db:"name"`
8 | Email string `db:"email"`
9 | CreatedAt time.Time `db:"created_at"`
10 | UpdatedAt time.Time `db:"updated_at"`
11 | }
12 |
--------------------------------------------------------------------------------
/model/comment.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | type Comment struct {
6 | ID int `db:"id"`
7 | Text string `db:"text"`
8 | User User `db:"user"`
9 | CreatedAt time.Time `db:"created_at"`
10 | UpdatedAt time.Time `db:"updated_at"`
11 | }
12 |
--------------------------------------------------------------------------------
/model/post.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | type Post struct {
6 | ID int `db:"id"`
7 | Text string `db:"text"`
8 | Title string `db:"title"`
9 | User User `db:"user"`
10 | Comments []Comment `db:"comments"`
11 | CreatedAt time.Time `db:"created_at"`
12 | UpdatedAt time.Time `db:"updated_at"`
13 | }
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.21 as builder
2 |
3 | ENV CGO_ENABLED=0
4 | ENV GOOS=linux
5 | ENV GOARCH=amd64
6 | WORKDIR /build
7 |
8 | COPY go.mod ./
9 | COPY go.sum ./
10 | RUN go mod download
11 |
12 | COPY . .
13 |
14 | RUN go build -o main
15 |
16 | #
17 | # Deploy
18 | #
19 | # hadolint ignore=DL3007
20 | FROM gcr.io/distroless/static-debian11:latest
21 |
22 | ENV TZ=Asia/Tokyo
23 |
24 | WORKDIR /
25 |
26 | COPY --from=builder /build/main /main
27 | COPY --from=builder /build/template /template
28 |
29 | USER nonroot
30 |
31 | EXPOSE 8080
32 |
33 | CMD [ "/main" ]
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | # MySQL
5 | db:
6 | image: mysql:8
7 | container_name: mysql_host
8 | environment:
9 | MYSQL_ROOT_PASSWORD: root
10 | MYSQL_DATABASE: micro_post
11 | MYSQL_USER: ojisan
12 | MYSQL_PASSWORD: ojisan
13 | TZ: 'Asia/Tokyo'
14 | command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
15 | volumes:
16 | - ./db/data:/var/lib/mysql
17 | - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
18 | - ./db/sql:/docker-entrypoint-initdb.d
19 | ports:
20 | - 3306:3306
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
23 | /db/data
24 | /db/my.cnf
25 | .env
--------------------------------------------------------------------------------
/template/_header.html:
--------------------------------------------------------------------------------
1 | {{define "header"}}
2 |
10 |
14 |
15 |
{{.Name}}でログイン中
16 |
19 |
20 |
21 | {{end}}
22 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "learn-go-server/router"
5 | "log"
6 | "net/http"
7 |
8 | _ "github.com/go-sql-driver/mysql"
9 | )
10 |
11 | func main() {
12 | http.HandleFunc("/", router.HomeHandler)
13 | // for /users/:id
14 | http.HandleFunc("/users", router.UsersHandler)
15 | http.HandleFunc("/users/", router.UsersDetailHandler)
16 |
17 | http.HandleFunc("/posts", router.PostsHandler)
18 | http.HandleFunc("/posts/", router.PostsDetailHandler)
19 | http.HandleFunc("/posts/new", router.PostsNewHandler)
20 |
21 | http.HandleFunc("/signout", router.SignoutHandler)
22 |
23 | log.Fatal(http.ListenAndServe(":8080", nil))
24 | }
25 |
--------------------------------------------------------------------------------
/template/users-new.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Goちゃんねる
8 |
9 |
10 | user 登録
11 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/template/posts.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Goちゃんねる
8 |
12 |
13 |
14 | {{template "header" .User}} {{ range .Posts }}
15 |
24 | {{ end }}
25 |
26 |
27 |
--------------------------------------------------------------------------------
/router/signout.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "database/sql"
5 | "log"
6 | "net/http"
7 | "os"
8 | "time"
9 | )
10 |
11 | // どんな結果だろうと必ずクッキーを消すようにする。early return しない。
12 | func SignoutHandler(w http.ResponseWriter, r *http.Request) {
13 | dsn := os.Getenv("dbdsn")
14 | db, err := sql.Open("mysql", dsn)
15 | if err != nil {
16 | log.Printf("ERROR: db open err: %v", err)
17 | }
18 | defer db.Close()
19 |
20 | token, err := r.Cookie("token")
21 | if err != nil {
22 | log.Printf("ERROR: %v", err)
23 | }
24 |
25 | ins, err := db.Prepare("delete from session where token =?")
26 | if err != nil {
27 | log.Printf("ERROR: prepare token delete err: %v", err)
28 | }
29 | _, err = ins.Exec(token.Value)
30 | if err != nil {
31 | log.Printf("ERROR: exec token delete err: %v", err)
32 | }
33 |
34 | cookie := &http.Cookie{
35 | Name: "token",
36 | Expires: time.Now(),
37 | }
38 |
39 | http.SetCookie(w, cookie)
40 | http.Redirect(w, r, "/", http.StatusSeeOther)
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 sadnessOjisan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/template/posts-new.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Goちゃんねる
8 |
12 |
13 |
14 | {{template "header" .}}
15 | post 登録
16 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: deploy for server
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 |
8 | env:
9 | GCP_REGION: asia-northeast1
10 | IMAGE: asia.gcr.io/sandbox-296904/go-channel:${{ github.sha }}
11 | GCP_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS }}
12 | DOCKER_FILE_PATH: ./Dockerfile
13 | SERVICE_NAME: go-channel
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout the repository
20 | uses: actions/checkout@v3
21 | - name: Use Node.js
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: "18.x"
25 | - id: "auth"
26 | uses: "google-github-actions/auth@v0"
27 | with:
28 | credentials_json: "${{ env.GCP_CREDENTIALS }}"
29 | - name: Configure docker to use the gcloud cli
30 | run: gcloud auth configure-docker --quiet
31 | - name: Build a docker image
32 | run: docker build -t ${{ env.IMAGE }} -f ${{ env.DOCKER_FILE_PATH }} .
33 | - name: Push the docker image
34 | run: docker push ${{ env.IMAGE }}
35 | - name: Deploy to Cloud Run
36 | id: deploy
37 | uses: google-github-actions/deploy-cloudrun@v0
38 | with:
39 | service: ${{ env.SERVICE_NAME }}
40 | image: ${{ env.IMAGE }}
41 | region: ${{ env.GCP_REGION }}
42 |
--------------------------------------------------------------------------------
/db/sql/1_create-table.sql:
--------------------------------------------------------------------------------
1 | CREATE DATABASE IF NOT EXISTS micro_post;
2 |
3 | CREATE TABLE IF NOT EXISTS micro_post.users(
4 | `id` int(11) AUTO_INCREMENT,
5 | `name` varchar(32) NOT NULL UNIQUE,
6 | `password` text NOT NULL,
7 | `salt` text NOT NULL,
8 | `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
9 | `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
10 | PRIMARY KEY (id)
11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
12 |
13 | CREATE TABLE IF NOT EXISTS micro_post.session(
14 | `user_id` int(11) PRIMARY KEY,
15 | `token` text NOT NULL,
16 | `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
17 | `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
19 |
20 | CREATE TABLE IF NOT EXISTS micro_post.posts(
21 | `id` int(11) AUTO_INCREMENT,
22 | `title` varchar(32) NOT NULL,
23 | `text` text NOT NULL,
24 | `user_id` int(11) NOT NULL,
25 | `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
26 | `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
27 | PRIMARY KEY (id)
28 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
29 |
30 | CREATE TABLE IF NOT EXISTS micro_post.comments(
31 | `id` int(11) AUTO_INCREMENT,
32 | `text` text NOT NULL,
33 | `post_id` int(11) NOT NULL,
34 | `user_id` int(11) NOT NULL,
35 | `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
36 | `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
37 | PRIMARY KEY (id)
38 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--------------------------------------------------------------------------------
/template/post-detail.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Goちゃんねる
8 |
12 |
13 |
14 | {{template "header" .User}}
15 |
16 |
{{ .Post.Title }}
17 |
{{ .Post.Text }}
18 |
19 |
by {{.Post.User.Name}}
20 |
{{ .Post.CreatedAt.Format "2006/01/02 15:04" }}
21 |
22 |
23 |
28 | コメント
29 |
37 | 送信
38 |
39 |
40 | {{ range $i, $v := .Post.Comments }}
41 |
42 |
43 | {{ add $i 1 }}
44 | {{$v.User.Name}}
45 |
46 |
{{ $v.Text }}
47 |
{{ $v.CreatedAt.Format "2006/01/02 15:04" }}
48 |
49 | {{ end }}
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/router/home.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "database/sql"
5 | "html/template"
6 | "log"
7 | "net/http"
8 | "os"
9 | "time"
10 | )
11 |
12 | func HomeHandler(w http.ResponseWriter, r *http.Request) {
13 | token, err := r.Cookie("token")
14 | // cookie に token がないなら home ページを表示
15 | if err != nil {
16 | t := template.Must(template.ParseFiles("./template/home.html"))
17 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
18 | if err := t.Execute(w, nil); err != nil {
19 | log.Printf("ERROR: exec templating err: %v", err)
20 | w.WriteHeader(http.StatusInternalServerError)
21 | return
22 | }
23 | return
24 | }
25 |
26 | dsn := os.Getenv("dbdsn")
27 | db, err := sql.Open("mysql", dsn)
28 | defer db.Close()
29 | if err != nil {
30 | log.Printf("ERROR: db open err: %v", err)
31 | w.WriteHeader(http.StatusInternalServerError)
32 | return
33 | }
34 |
35 | row := db.QueryRow("select user_id from session where token = ? limit 1", token.Value)
36 | var user_id int
37 | if err := row.Scan(&user_id); err != nil {
38 | // token に紐づくユーザーがないので認証エラー。token リセットしてホームに戻す。
39 | cookie := &http.Cookie{
40 | Name: "token",
41 | Expires: time.Now(),
42 | }
43 |
44 | http.SetCookie(w, cookie)
45 | http.Redirect(w, r, "/", http.StatusSeeOther)
46 | return
47 | }
48 |
49 | // cookie の情報が session になかった場合
50 | if user_id == 0 {
51 | t := template.Must(template.ParseFiles("./template/home.html"))
52 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
53 | if err := t.Execute(w, nil); err != nil {
54 | log.Printf("ERROR: exec templating err: %v", err)
55 | w.WriteHeader(http.StatusInternalServerError)
56 | return
57 | }
58 | }
59 |
60 | // user 情報が見つかった時
61 | http.Redirect(w, r, "/posts", http.StatusSeeOther)
62 | }
63 |
--------------------------------------------------------------------------------
/template/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Goちゃんねる
8 |
12 |
13 |
14 |
15 |
Goちゃんねる
16 |
21 |
22 | ユーザー名
23 |
32 |
33 |
34 | パスワード
35 |
44 |
45 |
46 | はじめる
47 |
48 |
49 |
※一度登録したユーザーは同じユーザー名、パスワードでログインできます
50 |
51 |
52 | Go の勉強ように作っただけの掲示板です。感想ブログ
56 |
57 | ユーザー名の登録は早い者勝ちです。
58 |
59 | Friendly HTTP Error Pages なんてものはない。何かエラーが起きた時は
60 | HTTP Status Code からエスパーして解決してください
61 |
62 |
63 | サーバーを攻撃しない、誹謗中傷はしないなど、常識は守りましょう。
64 |
65 | サーバーのクレジットが尽きたら止まります。
66 |
67 | セキュリティ的に色々サボっています。全て自己責任でお願いします。
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/router/user.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/sha256"
6 | "database/sql"
7 | "encoding/hex"
8 | "encoding/json"
9 | "fmt"
10 | "learn-go-server/model"
11 | "log"
12 | "net/http"
13 | "os"
14 | "path/filepath"
15 | "strings"
16 | "time"
17 | "unicode/utf8"
18 | )
19 |
20 | // see: https://stackoverflow.com/questions/15130321/is-there-a-method-to-generate-a-uuid-with-go-language
21 | func pseudo_uuid() (uuid string) {
22 | b := make([]byte, 16)
23 | _, err := rand.Read(b)
24 | if err != nil {
25 | fmt.Println("Error: ", err)
26 | return
27 | }
28 |
29 | uuid = fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
30 | return
31 | }
32 |
33 | func UsersDetailHandler(w http.ResponseWriter, r *http.Request) {
34 | if r.Method != http.MethodGet {
35 | w.WriteHeader(http.StatusMethodNotAllowed)
36 | fmt.Printf("method not allowed")
37 | return
38 | }
39 | sub := strings.TrimPrefix(r.URL.Path, "/users")
40 | _, id := filepath.Split(sub)
41 | if id == "" {
42 | log.Printf("ERROR: user id is not found err")
43 | w.WriteHeader(http.StatusNotFound)
44 | return
45 | }
46 |
47 | dsn := os.Getenv("dbdsn")
48 | db, err := sql.Open("mysql", dsn)
49 | if err != nil {
50 | log.Printf("ERROR: db open err: %v", err)
51 | w.WriteHeader(http.StatusInternalServerError)
52 | return
53 | }
54 | row := db.QueryRow("select * from users where id = ? limit 1", id)
55 | defer db.Close()
56 |
57 | u := &model.User{}
58 | if err := row.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt, &u.UpdatedAt); err != nil {
59 | log.Printf("ERROR: db scan user err: %v", err)
60 | w.WriteHeader(http.StatusInternalServerError)
61 | return
62 | }
63 |
64 | w.Header().Set("Content-Type", "application/json; charset=utf-8")
65 | w.WriteHeader(http.StatusOK)
66 | if err := json.NewEncoder(w).Encode(u); err != nil {
67 | log.Println(err)
68 | }
69 | }
70 |
71 | func UsersHandler(w http.ResponseWriter, r *http.Request) {
72 | // POST users
73 | if r.Method == http.MethodPost {
74 | name := r.FormValue("name")
75 | if !(utf8.RuneCountInString(name) >= 1 && utf8.RuneCountInString(name) <= 32) {
76 | log.Printf("ERROR: name length is not invalid name: %s, utf8.RuneCountInString(name): %d", name, utf8.RuneCountInString(name))
77 | w.WriteHeader(http.StatusBadRequest)
78 | return
79 | }
80 |
81 | dsn := os.Getenv("dbdsn")
82 | db, err := sql.Open("mysql", dsn)
83 | defer db.Close()
84 | if err != nil {
85 | log.Printf("ERROR: db open err: %v", err)
86 | w.WriteHeader(http.StatusInternalServerError)
87 | return
88 | }
89 |
90 | exsist_user_row := db.QueryRow("select id, password, salt from users where name = ? limit 1", name)
91 | var user_id int
92 | var current_user_salt string
93 | var current_user_hashed_password string
94 | if err := exsist_user_row.Scan(&user_id, ¤t_user_hashed_password, ¤t_user_salt); err != nil && err != sql.ErrNoRows {
95 | log.Printf("ERROR: db scan user err: %v", err)
96 | w.WriteHeader(http.StatusInternalServerError)
97 | return
98 | }
99 |
100 | password := r.FormValue("password")
101 | if !(utf8.RuneCountInString(password) >= 1 && utf8.RuneCountInString(password) <= 100) {
102 | log.Printf("ERROR: title length is not invalid")
103 | w.WriteHeader(http.StatusBadRequest)
104 | return
105 | }
106 |
107 | if user_id == 0 {
108 | // アカウント情報が存在しないなら登録してクッキーを発行する
109 | salt := pseudo_uuid()
110 | password_added_salt := password + salt
111 | password_byte := []byte(password_added_salt)
112 | hasher := sha256.New()
113 | hasher.Write([]byte(password_byte))
114 | hashedPasswordString := hex.EncodeToString(hasher.Sum(nil))
115 |
116 | ins, err := db.Prepare("insert into users(name, password, salt) value (?, ?, ?)")
117 | if err != nil {
118 | log.Printf("ERROR: prepare users insert err: %v", err)
119 | w.WriteHeader(http.StatusInternalServerError)
120 | return
121 | }
122 |
123 | res, err := ins.Exec(name, hashedPasswordString, salt)
124 | if err != nil {
125 | log.Printf("ERROR: exec user insert err: %v", err)
126 | w.WriteHeader(http.StatusInternalServerError)
127 | return
128 | }
129 | added_user_id, err := res.LastInsertId()
130 | if err != nil {
131 | log.Printf("ERROR: get last user id err: %v", err)
132 | w.WriteHeader(http.StatusInternalServerError)
133 | return
134 | }
135 | uuid := pseudo_uuid()
136 |
137 | session_insert, err := db.Prepare("insert into session(user_id, token) value (?, ?)")
138 | if err != nil {
139 | log.Printf("ERROR: prepare session insert err: %v", err)
140 | w.WriteHeader(http.StatusInternalServerError)
141 | return
142 | }
143 |
144 | _, err = session_insert.Exec(added_user_id, uuid)
145 | if err != nil {
146 | log.Printf("ERROR: exec session insert err: %v", err)
147 | w.WriteHeader(http.StatusInternalServerError)
148 | return
149 | }
150 |
151 | cookie := &http.Cookie{
152 | Name: "token",
153 | Value: uuid,
154 | Expires: time.Now().AddDate(0, 0, 1),
155 | SameSite: http.SameSiteStrictMode,
156 | HttpOnly: true,
157 | Secure: true,
158 | }
159 | http.SetCookie(w, cookie)
160 | http.Redirect(w, r, "/posts", http.StatusSeeOther)
161 | return
162 | } else {
163 | // アカウント情報が存在するユーザーなら、入力されたパスワードと正しいか確認してから、クッキー発行してログインさせる
164 | password_added_salt := password + current_user_salt
165 | password_byte := []byte(password_added_salt)
166 | hasher := sha256.New()
167 | hasher.Write([]byte(password_byte))
168 | hashedPasswordString := hex.EncodeToString(hasher.Sum(nil))
169 |
170 | if current_user_hashed_password != hashedPasswordString {
171 | log.Printf("ERROR: user input password mismatch")
172 | w.WriteHeader(http.StatusUnauthorized)
173 | return
174 | }
175 |
176 | uuid := pseudo_uuid()
177 | session_insert, err := db.Prepare("insert into session(user_id, token) value (?, ?)")
178 | if err != nil {
179 | log.Printf("ERROR: prepare session insert err: %v", err)
180 | w.WriteHeader(http.StatusInternalServerError)
181 | return
182 | }
183 |
184 | _, err = session_insert.Exec(user_id, uuid)
185 | if err != nil {
186 | log.Printf("ERROR: exec session insert err: %v", err)
187 | w.WriteHeader(http.StatusInternalServerError)
188 | return
189 | }
190 | cookie := &http.Cookie{
191 | Name: "token",
192 | Value: uuid,
193 | Expires: time.Now().AddDate(0, 0, 1),
194 | SameSite: http.SameSiteStrictMode,
195 | HttpOnly: true,
196 | Secure: true,
197 | }
198 | http.SetCookie(w, cookie)
199 | http.Redirect(w, r, "/posts", http.StatusSeeOther)
200 | return
201 | }
202 |
203 | }
204 |
205 | // GET /posts
206 | if r.Method == http.MethodGet {
207 |
208 | dsn := os.Getenv("dbdsn")
209 | db, err := sql.Open("mysql", dsn)
210 | defer db.Close()
211 | if err != nil {
212 | log.Printf("ERROR: db open err: %v", err)
213 | w.WriteHeader(http.StatusInternalServerError)
214 | return
215 | }
216 | rows, err := db.Query("select * from users")
217 | if err != nil {
218 | log.Printf("ERROR: exec users query err: %v", err)
219 | w.WriteHeader(http.StatusInternalServerError)
220 | return
221 | }
222 |
223 | var users []model.User
224 | for rows.Next() {
225 | u := &model.User{}
226 | if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt, &u.UpdatedAt); err != nil {
227 | log.Printf("ERROR: db scan users err: %v", err)
228 | w.WriteHeader(http.StatusInternalServerError)
229 | return
230 | }
231 | users = append(users, *u)
232 | }
233 |
234 | w.Header().Set("Content-Type", "application/json; charset=utf-8")
235 | w.WriteHeader(http.StatusOK)
236 | if err := json.NewEncoder(w).Encode(users); err != nil {
237 | log.Println(err)
238 | }
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/router/posts.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "html/template"
7 | "learn-go-server/model"
8 | "log"
9 | "net/http"
10 | "os"
11 | "path/filepath"
12 | "strings"
13 | "time"
14 | "unicode/utf8"
15 | )
16 |
17 | func PostsNewHandler(w http.ResponseWriter, r *http.Request) {
18 | if r.Method != http.MethodGet {
19 | log.Printf("ERROR: invalid method")
20 | w.WriteHeader(http.StatusMethodNotAllowed)
21 | return
22 | }
23 |
24 | token, err := r.Cookie("token")
25 | if err != nil {
26 | log.Printf("ERROR: %v", err)
27 | w.WriteHeader(http.StatusUnauthorized)
28 | return
29 | }
30 |
31 | dsn := os.Getenv("dbdsn")
32 | db, err := sql.Open("mysql", dsn)
33 | if err != nil {
34 | log.Printf("ERROR: db open err: %v", err)
35 | w.WriteHeader(http.StatusInternalServerError)
36 | return
37 | }
38 | defer db.Close()
39 | signin_user_query := `
40 | select
41 | users.id, users.name
42 | from
43 | session
44 | inner join
45 | users
46 | on
47 | users.id = session.user_id
48 | where
49 | token = ?
50 | `
51 | row := db.QueryRow(signin_user_query, token.Value)
52 | u := &model.User{}
53 | if err := row.Scan(&u.ID, &u.Name); err != nil {
54 | // token に紐づくユーザーがないので認証エラー。token リセットしてホームに戻す。
55 | cookie := &http.Cookie{
56 | Name: "token",
57 | Expires: time.Now(),
58 | }
59 |
60 | http.SetCookie(w, cookie)
61 | http.Redirect(w, r, "/", http.StatusSeeOther)
62 | return
63 | }
64 |
65 | t := template.Must(template.ParseFiles("./template/posts-new.html", "./template/_header.html"))
66 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
67 | if err := t.Execute(w, u); err != nil {
68 | log.Printf("ERROR: exec templating err: %v", err)
69 | w.WriteHeader(http.StatusInternalServerError)
70 | return
71 | }
72 | }
73 |
74 | func PostsDetailHandler(w http.ResponseWriter, r *http.Request) {
75 | // GET /posts/:id
76 | if r.Method == http.MethodGet {
77 | token, err := r.Cookie("token")
78 | if err != nil {
79 | log.Println(err)
80 | w.WriteHeader(http.StatusUnauthorized)
81 | return
82 | }
83 |
84 | dsn := os.Getenv("dbdsn")
85 | db, err := sql.Open("mysql", dsn)
86 | defer db.Close()
87 | if err != nil {
88 | log.Printf("ERROR: db open err: %v", err)
89 | w.WriteHeader(http.StatusInternalServerError)
90 | return
91 | }
92 | signin_user_query := `
93 | select
94 | users.id, users.name
95 | from
96 | session
97 | inner join
98 | users
99 | on
100 | users.id = session.user_id
101 | where
102 | token = ?
103 | `
104 | row := db.QueryRow(signin_user_query, token.Value)
105 | u := &model.User{}
106 | if err := row.Scan(&u.ID, &u.Name); err != nil {
107 | // token に紐づくユーザーがないので認証エラー。token リセットしてホームに戻す。
108 | cookie := &http.Cookie{
109 | Name: "token",
110 | Expires: time.Now(),
111 | }
112 |
113 | http.SetCookie(w, cookie)
114 | http.Redirect(w, r, "/", http.StatusSeeOther)
115 | return
116 | }
117 |
118 | sub := strings.TrimPrefix(r.URL.Path, "/posts")
119 | _, id := filepath.Split(sub)
120 | if id == "" {
121 | log.Printf("ERROR: post id not found err: %v", err)
122 | w.WriteHeader(http.StatusNotFound)
123 | return
124 | }
125 |
126 | query := `
127 | select
128 | p.id, p.title, p.text, p.created_at, p.updated_at,
129 | post_user.id, post_user.name,
130 | c.id as comment_id, c.text as comment_text, c.created_at as comment_created_at, c.updated_at as comment_updated_at,
131 | comment_user.id, comment_user.name
132 | from
133 | posts p
134 | join
135 | users post_user
136 | on
137 | p.user_id = post_user.id
138 | left join
139 | comments c
140 | on
141 | p.id = c.post_id
142 | left join
143 | users comment_user
144 | on
145 | c.user_id = comment_user.id
146 | where
147 | p.id = ?
148 | `
149 | rows, err := db.Query(query, id)
150 | if err != nil {
151 | log.Printf("ERROR: exec posts query err: %v", err)
152 | w.WriteHeader(http.StatusInternalServerError)
153 | return
154 | }
155 |
156 | post := &model.Post{}
157 | for rows.Next() {
158 | fmt.Println("rows scan")
159 | post_user := &model.User{}
160 | comment_dto := &struct {
161 | ID sql.NullInt16
162 | Text sql.NullString
163 | CreatedAt sql.NullTime
164 | UpdatedAt sql.NullTime
165 | }{}
166 | user_dto := &struct {
167 | ID sql.NullInt16
168 | Name sql.NullString
169 | }{}
170 | err = rows.Scan(
171 | &post.ID, &post.Title, &post.Text, &post.CreatedAt, &post.UpdatedAt,
172 | &post_user.ID, &post_user.Name,
173 | &comment_dto.ID, &comment_dto.Text, &comment_dto.CreatedAt, &comment_dto.UpdatedAt,
174 | &user_dto.ID, &user_dto.Name,
175 | )
176 | if err != nil {
177 | log.Printf("ERROR: posts db scan err: %v", err)
178 | w.WriteHeader(http.StatusInternalServerError)
179 | return
180 | }
181 | post.User = *post_user
182 | if comment_dto.ID.Int16 != 0 {
183 | post.Comments = append(post.Comments, model.Comment{
184 | ID: int(comment_dto.ID.Int16),
185 | Text: comment_dto.Text.String,
186 | CreatedAt: comment_dto.CreatedAt.Time,
187 | UpdatedAt: comment_dto.UpdatedAt.Time,
188 | User: model.User{
189 | ID: int(user_dto.ID.Int16),
190 | Name: user_dto.Name.String,
191 | },
192 | })
193 | }
194 |
195 | }
196 |
197 | funcs := template.FuncMap{
198 | "add": func(a, b int) int {
199 | return a + b
200 | },
201 | }
202 | // NOTE: .Func を呼ぶ位置に注意
203 | t := template.Must(template.New("post-detail.html").Funcs(funcs).ParseFiles("./template/post-detail.html", "./template/_header.html"))
204 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
205 |
206 | if err := t.Execute(w, struct {
207 | model.Post
208 | model.User
209 | }{Post: *post, User: *u}); err != nil {
210 | log.Printf("ERROR: exec templating err: %v", err)
211 | w.WriteHeader(http.StatusInternalServerError)
212 | return
213 | }
214 | return
215 | }
216 |
217 | // POST /posts/:id/comments
218 | if r.Method == http.MethodPost {
219 | text := r.FormValue("text")
220 | if !(utf8.RuneCountInString(text) >= 1 && utf8.RuneCountInString(text) <= 1000) {
221 | log.Printf("ERROR: text length is not invalid")
222 | w.WriteHeader(http.StatusBadRequest)
223 | return
224 | }
225 |
226 | segments := strings.Split(r.URL.Path, "/")
227 | if len(segments) != 4 || segments[2] == "" || segments[3] != "comments" {
228 | http.NotFound(w, r)
229 | return
230 | }
231 | post_id := segments[2]
232 |
233 | dsn := os.Getenv("dbdsn")
234 | db, err := sql.Open("mysql", dsn)
235 | defer db.Close()
236 | if err != nil {
237 | log.Printf("ERROR: db open err: %v", err)
238 | w.WriteHeader(http.StatusInternalServerError)
239 | return
240 | }
241 |
242 | token, err := r.Cookie("token")
243 | if err != nil {
244 | log.Println(err)
245 | w.WriteHeader(http.StatusUnauthorized)
246 | return
247 | }
248 |
249 | row := db.QueryRow("select user_id from session where token = ? limit 1", token.Value)
250 | var user_id int
251 | if err := row.Scan(&user_id); err != nil {
252 | log.Printf("ERROR: db scan user err: %v", err)
253 | w.WriteHeader(http.StatusUnauthorized)
254 | return
255 | }
256 |
257 | ins, err := db.Prepare("insert into comments(text, post_id, user_id) value (?, ?, ?)")
258 | if err != nil {
259 | log.Printf("ERROR: prepare comment insert err: %v", err)
260 | w.WriteHeader(http.StatusInternalServerError)
261 | return
262 | }
263 | _, err = ins.Exec(text, post_id, user_id)
264 | if err != nil {
265 | log.Printf("ERROR: exec comment insert err: %v", err)
266 | w.WriteHeader(http.StatusInternalServerError)
267 | return
268 | }
269 |
270 | http.Redirect(w, r, fmt.Sprintf("/posts/%s", post_id), http.StatusSeeOther)
271 | return
272 | }
273 | }
274 |
275 | func PostsHandler(w http.ResponseWriter, r *http.Request) {
276 | // POST /posts
277 | if r.Method == http.MethodPost {
278 | token, err := r.Cookie("token")
279 | if err != nil {
280 | log.Println(err)
281 | }
282 | title := r.FormValue("title")
283 | text := r.FormValue("text")
284 |
285 | if !(utf8.RuneCountInString(title) >= 1 && utf8.RuneCountInString(title) <= 100) {
286 | log.Printf("ERROR: title length is not invalid")
287 | w.WriteHeader(http.StatusBadRequest)
288 | return
289 | }
290 |
291 | if !(utf8.RuneCountInString(text) >= 1 && utf8.RuneCountInString(text) <= 1000) {
292 | log.Printf("ERROR: text length is not invalid")
293 | w.WriteHeader(http.StatusBadRequest)
294 | return
295 | }
296 |
297 | dsn := os.Getenv("dbdsn")
298 | db, err := sql.Open("mysql", dsn)
299 | if err != nil {
300 | log.Printf("ERROR: db open err: %v", err)
301 | w.WriteHeader(http.StatusInternalServerError)
302 | return
303 | }
304 | defer db.Close()
305 |
306 | row := db.QueryRow("select user_id from session where token = ? limit 1", token.Value)
307 | var userID int
308 | if err := row.Scan(&userID); err != nil {
309 | log.Printf("ERROR: db scan user err: %v", err)
310 | w.WriteHeader(http.StatusUnauthorized)
311 | return
312 | }
313 |
314 | ins, err := db.Prepare("insert into posts(title, text, user_id) value (?, ?, ?)")
315 | if err != nil {
316 | log.Printf("ERROR: prepare posts insert err: %v", err)
317 | w.WriteHeader(http.StatusInternalServerError)
318 | return
319 | }
320 | res, err := ins.Exec(title, text, userID)
321 | if err != nil {
322 | log.Printf("ERROR: exec post insert err: %v", err)
323 | w.WriteHeader(http.StatusInternalServerError)
324 | return
325 | }
326 | post_id, err := res.LastInsertId()
327 | if err != nil {
328 | log.Printf("ERROR: exec get post last id err: %v", err)
329 | w.WriteHeader(http.StatusInternalServerError)
330 | return
331 | }
332 | http.Redirect(w, r, fmt.Sprintf("posts/%d", post_id), http.StatusSeeOther)
333 | return
334 | }
335 |
336 | // GET /posts
337 | if r.Method == http.MethodGet {
338 | token, err := r.Cookie("token")
339 | if err != nil {
340 | log.Println(err)
341 | w.WriteHeader(http.StatusUnauthorized)
342 | return
343 | }
344 |
345 | dsn := os.Getenv("dbdsn")
346 | db, err := sql.Open("mysql", dsn)
347 | defer db.Close()
348 | if err != nil {
349 | log.Printf("ERROR: db open err: %v", err)
350 | w.WriteHeader(http.StatusInternalServerError)
351 | return
352 | }
353 | signin_user_query := `
354 | select
355 | users.id, users.name
356 | from
357 | session
358 | inner join
359 | users
360 | on
361 | users.id = session.user_id
362 | where
363 | token = ?
364 | `
365 | row := db.QueryRow(signin_user_query, token.Value)
366 | u := &model.User{}
367 | if err := row.Scan(&u.ID, &u.Name); err != nil {
368 | log.Printf("ERROR: db scan user err: %v", err)
369 | w.WriteHeader(http.StatusUnauthorized)
370 | return
371 | }
372 |
373 | rows, err := db.Query(`
374 | select
375 | p.id, p.title, p.text, p.created_at, p.updated_at,
376 | u.id as user_id, u.name as user_name
377 | from
378 | posts p
379 | inner join
380 | users u
381 | on
382 | user_id = u.id
383 | order by
384 | p.created_at desc
385 | `)
386 | if err != nil {
387 | log.Printf("ERROR: exec posts query err: %v", err)
388 | w.WriteHeader(http.StatusInternalServerError)
389 | return
390 | }
391 | defer db.Close()
392 |
393 | var posts []model.Post
394 | for rows.Next() {
395 | p := &model.Post{}
396 | u := &model.User{}
397 | if err := rows.Scan(&p.ID, &p.Title, &p.Text, &p.CreatedAt, &p.UpdatedAt, &u.ID, &u.Name); err != nil {
398 | log.Printf("ERROR: db scan post err: %v", err)
399 | w.WriteHeader(http.StatusInternalServerError)
400 | return
401 | }
402 | p.User = *u
403 | posts = append(posts, *p)
404 | }
405 |
406 | t := template.Must(template.ParseFiles("./template/posts.html", "./template/_header.html"))
407 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
408 | if err := t.Execute(w, struct {
409 | Posts []model.Post
410 | model.User
411 | }{Posts: posts, User: *u}); err != nil {
412 | log.Printf("ERROR: exec templating err: %v", err)
413 | w.WriteHeader(http.StatusInternalServerError)
414 | return
415 | }
416 | }
417 | }
418 |
--------------------------------------------------------------------------------