├── 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 |
11 | 新規投稿 12 | 投稿一覧 13 |
14 |
15 |

{{.Name}}でログイン中

16 |
17 | 18 |
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 |
12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /template/posts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Goちゃんねる 8 | 12 | 13 | 14 | {{template "header" .User}} {{ range .Posts }} 15 |
16 | 17 |

{{ .Title }}

18 |
19 |

by {{ .User.Name }}

20 |

{{ .CreatedAt.Format "2006/01/02 15:04" }}

21 |
22 |
23 |
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 |
17 | 18 | 27 | 28 | 37 | 38 |
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 | 48 |
49 |

※一度登録したユーザーは同じユーザー名、パスワードでログインできます

50 | 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 | --------------------------------------------------------------------------------