freckie

Add: first commit

1 +package main
2 +
3 +import (
4 + "encoding/json"
5 + "io/ioutil"
6 +)
7 +
8 +type Config struct {
9 + Server ConfigServer `json:"server"`
10 + Database ConfigDatabase `json:"database"`
11 +}
12 +
13 +type ConfigServer struct {
14 + LocalMode bool `json:"local_mode"`
15 + Port int `json:"port"`
16 +}
17 +
18 +type ConfigDatabase struct {
19 + Host string `json:"host"`
20 + Port int `json:"port"`
21 + User string `json:"user"`
22 + Password string `json:"password"`
23 + Schema string `json:"schema"`
24 +}
25 +
26 +func LoadConfig(filePath string) (*Config, error) {
27 + cfg := &Config{}
28 +
29 + dataBytes, err := ioutil.ReadFile(filePath)
30 + if err != nil {
31 + return cfg, err
32 + }
33 +
34 + json.Unmarshal(dataBytes, cfg)
35 +
36 + return cfg, nil
37 +}
1 +package endpoints
2 +
3 +import (
4 + "classroom/functions"
5 + "classroom/models"
6 + "database/sql"
7 + "fmt"
8 + "net/http"
9 + "strconv"
10 + "strings"
11 +
12 + "github.com/julienschmidt/httprouter"
13 +)
14 +
15 +// GET /timetables/<file_id>/<sheet_id>/cell
16 +func (e *Endpoints) CellGet(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
17 + // Get user email
18 + var email string
19 + if _email, ok := r.Header["X-User-Email"]; ok {
20 + email = _email[0]
21 + } else {
22 + functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
23 + return
24 + }
25 +
26 + // Get Path Parameters
27 + fileID := ps.ByName("file_id")
28 + sheetID := ps.ByName("sheet_id")
29 +
30 + // Get Query Parameters
31 + qp := r.URL.Query()
32 + var cellColumn string
33 + var cellStart int
34 + var cellEnd int
35 + var err error
36 +
37 + if _cellColumn, ok := qp["column"]; ok {
38 + cellColumn = strings.ToUpper(_cellColumn[0])
39 + } else {
40 + functions.ResponseError(w, 400, "column 파라미터를 보내세요.")
41 + return
42 + }
43 +
44 + if _cellStart, ok := qp["start"]; ok {
45 + cellStart, err = strconv.Atoi(_cellStart[0])
46 + if err != nil {
47 + functions.ResponseError(w, 400, "start 파라미터를 보내세요.")
48 + return
49 + }
50 + }
51 + if _cellEnd, ok := qp["end"]; ok {
52 + cellEnd, err = strconv.Atoi(_cellEnd[0])
53 + if err != nil {
54 + functions.ResponseError(w, 400, "end 파라미터를 보내세요.")
55 + return
56 + }
57 + }
58 +
59 + // Check Permission
60 + var _timetable, _email string
61 + timetable := fmt.Sprintf("%s,%s", fileID, sheetID)
62 + row := e.DB.QueryRow(`
63 + SELECT a.timetable_id, u.email
64 + FROM allowlist AS a, users AS u
65 + WHERE a.timetable_id=?
66 + AND a.user_id=u.id;
67 + `, timetable)
68 + if err := row.Scan(&_timetable, &_email); err != nil {
69 + if err == sql.ErrNoRows {
70 + functions.ResponseError(w, 404, "존재하지 않은 timetable.")
71 + return
72 + }
73 + }
74 + if _email != email {
75 + functions.ResponseError(w, 403, "timetable 접근 권한 부족")
76 + return
77 + }
78 +
79 + // Result Resp
80 + resp := models.CellGetResponse{}
81 + resp.Cells = []models.CellItem{}
82 +
83 + // Querying
84 + rows, err := e.DB.Query(`
85 + SELECT u.email, u.id, t.cell_column, t.cell_start, t.cell_end, t.lecture, t.professor, t.transaction_id, t.created_at, t.capacity
86 + FROM transactions AS t, users AS u
87 + WHERE t.user_id=u.id
88 + AND u.email=?
89 + AND t.transaction_type=1
90 + AND t.timetable_id=?
91 + AND t.cell_column=?;`, email, timetable, cellColumn)
92 + if err != nil {
93 + if err == sql.ErrNoRows {
94 + resp.CellsCount = 0
95 + functions.ResponseOK(w, "success", resp)
96 + return
97 + }
98 + functions.ResponseError(w, 500, err.Error())
99 + return
100 + }
101 + defer rows.Close()
102 +
103 + cells := []models.CellTransactionModel{}
104 + for rows.Next() {
105 + temp := models.CellTransactionModel{}
106 + err := rows.Scan(&temp.UserEmail, &temp.UserID, &temp.CellColumn, &temp.CellStart, &temp.CellEnd, &temp.Lecture, &temp.Professor, &temp.TransactionID, &temp.CreatedAt, &temp.Capacity)
107 + if err != nil {
108 + continue
109 + }
110 + cells = append(cells, temp)
111 + }
112 +
113 + // Compare
114 + for i := cellStart; i <= cellEnd; i++ {
115 + isInRange := false
116 + for _, cell := range cells {
117 + if functions.InRange(i, cell.CellStart, cell.CellEnd) {
118 + temp := models.CellItem{}
119 + temp.Cell = fmt.Sprintf("%s%d", cellColumn, i)
120 + temp.IsReserved = true
121 + temp.UserEmail = cell.UserEmail
122 + temp.UserID = cell.UserID
123 + temp.Lecture = cell.Lecture
124 + temp.Professor = cell.Professor
125 + temp.TransactionID = cell.TransactionID
126 + temp.CreatedAt = functions.ToKST(cell.CreatedAt)
127 + temp.Capacity = cell.Capacity
128 +
129 + resp.Cells = append(resp.Cells, temp)
130 + isInRange = true
131 + break
132 + }
133 + }
134 +
135 + if !isInRange {
136 + temp := models.CellItem{}
137 + temp.Cell = fmt.Sprintf("%s%d", cellColumn, i)
138 + temp.IsReserved = false
139 +
140 + resp.Cells = append(resp.Cells, temp)
141 + }
142 + }
143 +
144 + // Struct for response
145 + resp.CellsCount = len(resp.Cells)
146 +
147 + functions.ResponseOK(w, "success", resp)
148 +}
1 +package endpoints
2 +
3 +import (
4 + "database/sql"
5 +)
6 +
7 +type Endpoints struct {
8 + DB *sql.DB
9 +}
1 +module classroom/endpoints
2 +
3 +go 1.14
4 +
5 +require (
6 + classroom/functions v0.0.0
7 + github.com/julienschmidt/httprouter v1.3.0
8 +)
9 +
10 +replace classroom/functions v0.0.0 => ../functions
1 +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
2 +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
1 +package endpoints
2 +
3 +import (
4 + "classroom/functions"
5 + "classroom/models"
6 + "net/http"
7 +
8 + "github.com/julienschmidt/httprouter"
9 +)
10 +
11 +// GET /
12 +func (e *Endpoints) IndexGet(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
13 + w.Header().Set("Content-Type", "application/json")
14 +
15 + // Struct for response
16 + resp := models.IndexResponse{}
17 + resp.WelcomeMessage = "Hello, Kyung Hee!"
18 +
19 + // Response with JSON
20 + functions.ResponseOK(w, "success", resp)
21 +}
1 +package endpoints
2 +
3 +import (
4 + "classroom/functions"
5 + "classroom/models"
6 + "database/sql"
7 + "encoding/json"
8 + "fmt"
9 + "io/ioutil"
10 + "net/http"
11 + "strings"
12 +
13 + "github.com/julienschmidt/httprouter"
14 +)
15 +
16 +// POST /timetables/<file_id>/<sheet_id>/reservation
17 +func (e *Endpoints) ReservationPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
18 + // Get user email
19 + var email string
20 + if _email, ok := r.Header["X-User-Email"]; ok {
21 + email = _email[0]
22 + } else {
23 + functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
24 + return
25 + }
26 +
27 + // Get Path Parameters
28 + fileID := ps.ByName("file_id")
29 + sheetID := ps.ByName("sheet_id")
30 +
31 + // Check Permission
32 + var _timetable, _email string
33 + var userID int
34 + timetable := fmt.Sprintf("%s,%s", fileID, sheetID)
35 + row := e.DB.QueryRow(`
36 + SELECT a.timetable_id, u.email, u.id
37 + FROM allowlist AS a, users AS u
38 + WHERE a.timetable_id=?
39 + AND a.user_id=u.id;
40 + `, timetable)
41 + if err := row.Scan(&_timetable, &_email, &userID); err != nil {
42 + if err == sql.ErrNoRows {
43 + functions.ResponseError(w, 404, "존재하지 않는 timetable")
44 + return
45 + }
46 + functions.ResponseError(w, 500, "예기치 못한 에러 발생 : "+err.Error())
47 + return
48 + }
49 + if _email != email {
50 + functions.ResponseError(w, 403, "timetable 접근 권한 부족")
51 + return
52 + }
53 +
54 + // Parse Request Data
55 + type reqDataStruct struct {
56 + Column *string `json:"column"`
57 + Start *int `json:"start"`
58 + End *int `json:"end"`
59 + Lecture *string `json:"lecture"`
60 + Professor *string `json:"professor"`
61 + Capacity *int `json:"capacity"`
62 + }
63 + var reqData reqDataStruct
64 + if strings.Contains(r.Header.Get("Content-Type"), "application/json") {
65 + body, err := ioutil.ReadAll(r.Body)
66 + if err != nil {
67 + functions.ResponseError(w, 500, err.Error())
68 + }
69 + json.Unmarshal(body, &reqData)
70 + } else {
71 + functions.ResponseError(w, 400, "JSON 형식만 가능합니다.")
72 + return
73 + }
74 + if reqData.Column == nil || reqData.Start == nil || reqData.End == nil ||
75 + reqData.Lecture == nil || reqData.Professor == nil || reqData.Capacity == nil {
76 + functions.ResponseError(w, 400, "파라미터를 전부 보내주세요.")
77 + return
78 + }
79 +
80 + // Querying (Cell Validation Check)
81 + isPossible := true
82 + rows, err := e.DB.Query(`
83 + SELECT cell_start, cell_end
84 + FROM transactions
85 + WHERE transaction_type=1
86 + AND cell_column=?;
87 + `, *(reqData.Column))
88 + if err == sql.ErrNoRows {
89 + isPossible = true
90 + }
91 + defer rows.Close()
92 +
93 +loopCheckingValidation:
94 + for rows.Next() {
95 + var _start, _end int
96 + err = rows.Scan(&_start, &_end)
97 + if err != nil {
98 + continue
99 + }
100 + for i := *(reqData.Start); i <= *(reqData.End); i++ {
101 + if functions.InRange(i, _start, _end) {
102 + isPossible = false
103 + break loopCheckingValidation
104 + }
105 + }
106 + }
107 +
108 + if !isPossible {
109 + functions.ResponseError(w, 500, "해당 셀 범위에 예약이 존재합니다.")
110 + return
111 + }
112 +
113 + // Result Resp
114 + resp := models.ReservationPostResponse{}
115 +
116 + // Querying (Making a Transaction)
117 + res, err := e.DB.Exec(`
118 + INSERT INTO transactions (transaction_type, user_id, timetable_id, lecture, capacity, cell_column, cell_start, cell_end, professor)
119 + VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?);
120 + `, userID, timetable, *(reqData.Lecture), *(reqData.Capacity), *(reqData.Column), *(reqData.Start), *(reqData.End), *(reqData.Professor))
121 + if err != nil {
122 + functions.ResponseError(w, 500, err.Error())
123 + return
124 + }
125 +
126 + resp.TransactionID, err = res.LastInsertId()
127 + if err != nil {
128 + functions.ResponseError(w, 500, err.Error())
129 + return
130 + }
131 +
132 + resp.IsSuccess = true
133 + resp.CellColumn = *(reqData.Column)
134 + resp.CellStart = *(reqData.Start)
135 + resp.CellEnd = *(reqData.End)
136 + resp.Lecture = *(reqData.Lecture)
137 + resp.Professor = *(reqData.Professor)
138 +
139 + functions.ResponseOK(w, "success", resp)
140 +}
141 +
142 +// DELETE /timetables/<file_id>/<sheet_id>/reservation/<reservation_id>
143 +func (e *Endpoints) ReservationDelete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
144 + // Get user email
145 + var email string
146 + if _email, ok := r.Header["X-User-Email"]; ok {
147 + email = _email[0]
148 + } else {
149 + functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
150 + return
151 + }
152 +
153 + // Get Path Parameters
154 + fileID := ps.ByName("file_id")
155 + sheetID := ps.ByName("sheet_id")
156 + reservationID := ps.ByName("reservation_id")
157 +
158 + // Check Timetable Permission
159 + var _timetable, _email string
160 + var userID int
161 + timetable := fmt.Sprintf("%s,%s", fileID, sheetID)
162 + row := e.DB.QueryRow(`
163 + SELECT a.timetable_id, u.email, u.id
164 + FROM allowlist AS a, users AS u
165 + WHERE a.timetable_id=?
166 + AND a.user_id=u.id;
167 + `, timetable)
168 + if err := row.Scan(&_timetable, &_email, &userID); err != nil {
169 + if err == sql.ErrNoRows {
170 + functions.ResponseError(w, 404, "존재하지 않는 timetable")
171 + return
172 + }
173 + functions.ResponseError(w, 500, "예기치 못한 에러 발생 : "+err.Error())
174 + return
175 + }
176 + if _email != email {
177 + functions.ResponseError(w, 403, "timetable 접근 권한 부족")
178 + return
179 + }
180 +
181 + // Check Transaction Permission
182 + var _transactionType int64
183 + row = e.DB.QueryRow(`
184 + SELECT u.email, t.transaction_type
185 + FROM transactions AS t, users AS u
186 + WHERE t.user_id=u.id
187 + AND t.transaction_id=?;
188 + `, reservationID)
189 + err := row.Scan(&_email, &_transactionType)
190 + if err != nil {
191 + if err == sql.ErrNoRows {
192 + functions.ResponseError(w, 404, "존재하지 않는 예약")
193 + return
194 + }
195 + functions.ResponseError(w, 500, "예기치 못한 에러 발생 : "+err.Error())
196 + return
197 + }
198 + if _email != email {
199 + functions.ResponseError(w, 403, "예약 접근 권한 부족")
200 + return
201 + }
202 + if _transactionType == 0 {
203 + functions.ResponseError(w, 500, "이미 취소된 예약")
204 + return
205 + }
206 +
207 + // Querying
208 + res, err := e.DB.Exec(`
209 + UPDATE transactions SET transaction_type=0 WHERE transaction_id=?
210 + `, reservationID)
211 + if err != nil {
212 + functions.ResponseError(w, 500, err.Error())
213 + return
214 + }
215 + if affected, _ := res.RowsAffected(); affected != 1 {
216 + functions.ResponseError(w, 500, "예기치 못한 에러 발생. (RowsAffected != 1)")
217 + return
218 + }
219 +
220 + functions.ResponseOK(w, "success", nil)
221 +}
1 +package functions
2 +
3 +// InRange returns if the target is in the `range (start, end)
4 +func InRange(target, start, end int) bool {
5 + return (start <= target) && (target <= end)
6 +}
1 +module classroom/functions
2 +
3 +go 1.14
1 +package functions
2 +
3 +import (
4 + "encoding/json"
5 + "log"
6 + "net/http"
7 +
8 + "classroom/models"
9 +)
10 +
11 +// ResponseOK make 200 response.
12 +func ResponseOK(w http.ResponseWriter, msg string, data interface{}) {
13 + w.Header().Set("Content-Type", "application/json")
14 + w.Header().Set("Access-Control-Allow-Origin", "*")
15 + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
16 + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
17 + w.WriteHeader(200)
18 + json.NewEncoder(w).Encode(&models.Response{
19 + Status: 200,
20 + Message: msg,
21 + Data: data,
22 + })
23 +}
24 +
25 +// ResponseError make error response.
26 +func ResponseError(w http.ResponseWriter, errorCode int, errorMsg string) {
27 + log.Println("[Error] :", errorCode, errorMsg)
28 +
29 + w.Header().Set("Content-Type", "application/json")
30 + w.Header().Set("Access-Control-Allow-Origin", "*")
31 + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
32 + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
33 + w.WriteHeader(errorCode)
34 + json.NewEncoder(w).Encode(&models.Response{
35 + Status: errorCode,
36 + Message: errorMsg,
37 + Data: nil,
38 + })
39 +}
1 +package functions
2 +
3 +import (
4 + "time"
5 +)
6 +
7 +func ToKST(utc string) string {
8 + t, _ := time.Parse(time.RFC3339, utc)
9 + t = t.Add(9 * time.Hour)
10 + return t.Format("2006-01-02 15:04:05")
11 +}
1 +module classroom
2 +
3 +go 1.14
4 +
5 +require (
6 + github.com/go-sql-driver/mysql v1.5.0
7 + github.com/julienschmidt/httprouter v1.3.0
8 + github.com/rs/cors v1.7.0
9 +
10 + classroom/endpoints v0.0.0
11 + classroom/models v0.0.0
12 + classroom/functions v0.0.0
13 +)
14 +
15 +replace (
16 + classroom/endpoints v0.0.0 => ./endpoints
17 + classroom/models v0.0.0 => ./models
18 + classroom/functions v0.0.0 => ./functions
19 +)
...\ No newline at end of file ...\ No newline at end of file
1 +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
2 +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
3 +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
4 +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
5 +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
6 +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
1 +package main
2 +
3 +import (
4 + "database/sql"
5 + "fmt"
6 + "log"
7 + "net/http"
8 + "os"
9 + "strconv"
10 + "time"
11 +
12 + "classroom/endpoints"
13 + "classroom/functions"
14 +
15 + _ "github.com/go-sql-driver/mysql"
16 + "github.com/julienschmidt/httprouter"
17 + "github.com/rs/cors"
18 +)
19 +
20 +var logger *log.Logger
21 +
22 +type HostSwitch map[string]http.Handler
23 +
24 +func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
25 + if handler := hs[r.Host]; handler != nil {
26 + ip := r.RemoteAddr
27 + log.Println("[Req]", r.Method, r.URL, ip)
28 + handler.ServeHTTP(w, r)
29 + } else {
30 + functions.ResponseError(w, 403, "Forbidden hostname : "+r.Host)
31 + }
32 +}
33 +
34 +func main() {
35 + // Logger
36 + logger = log.New(os.Stdout, "LOG ", log.LstdFlags)
37 +
38 + // Config
39 + cfg, err := LoadConfig("config.json")
40 + if err != nil {
41 + log.Fatal(err)
42 + }
43 +
44 + // DB Setting
45 + if !(cfg.Server.LocalMode) {
46 + time.Sleep(time.Second * 10)
47 + }
48 + dbStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?allowNativePasswords=true&parseTime=true",
49 + cfg.Database.User,
50 + cfg.Database.Password,
51 + cfg.Database.Host,
52 + cfg.Database.Port,
53 + cfg.Database.Schema)
54 + db, err := sql.Open("mysql", dbStr)
55 + if err != nil {
56 + log.Fatal(err)
57 + }
58 + defer db.Close()
59 + ep := endpoints.Endpoints{DB: db}
60 +
61 + // Router Setting
62 + router := httprouter.New()
63 + router.GET("/api", ep.IndexGet)
64 + router.GET("/api/timetables/:file_id/:sheet_id/cell", ep.CellGet)
65 + router.POST("/api/timetables/:file_id/:sheet_id/reservation", ep.ReservationPost)
66 + router.DELETE("/api/timetables/:file_id/:sheet_id/reservation/:reservation_id", ep.ReservationDelete)
67 +
68 + // Local Mode
69 + portStr := strconv.Itoa(cfg.Server.Port)
70 + if cfg.Server.LocalMode {
71 + handler := cors.AllowAll().Handler(router)
72 +
73 + // Start Server in Local Mode
74 + log.Println("[Local Mode] Starting HTTP API Server on port", portStr)
75 + log.Fatal(http.ListenAndServe(":"+portStr, handler))
76 +
77 + } else { // Release Mode
78 + handler := cors.AllowAll().Handler(router)
79 + hs := make(HostSwitch)
80 + hs["web-api"] = handler
81 +
82 + // Start Server
83 + log.Println("[Release Mode] Starting HTTP API Server on port", portStr)
84 + log.Fatal(http.ListenAndServe(":"+portStr, hs))
85 + }
86 +}
1 +package models
2 +
3 +type CellTransactionModel struct {
4 + TransactionID int
5 + UserID int
6 + UserEmail string
7 + Lecture string
8 + CellColumn string
9 + CellStart int
10 + CellEnd int
11 + Professor string
12 + CreatedAt string
13 + Capacity int
14 +}
15 +
16 +type CellGetResponse struct {
17 + Cells []CellItem `json:"cells"`
18 + CellsCount int `json:"cells_count"`
19 +}
20 +
21 +type CellItem struct {
22 + Cell string `json:"cell"`
23 + IsReserved bool `json:"is_reserved"`
24 + UserEmail string `json:"user_email"`
25 + UserID int `json:"user_id"`
26 + Lecture string `json:"lecture"`
27 + Professor string `json:"professor"`
28 + Capacity int `json:"capacity"`
29 + TransactionID int `json:"transaction_id"`
30 + CreatedAt string `json:"created_at"`
31 +}
1 +module classroom/models
2 +
3 +go 1.14
1 +package models
2 +
3 +type IndexResponse struct {
4 + WelcomeMessage string `json:"welcome_message"`
5 +}
1 +package models
2 +
3 +type ReservationPostResponse struct {
4 + IsSuccess bool `json:"is_success"`
5 + TransactionID int64 `json:"transaction_id"`
6 + CellColumn string `json:"cell_column"`
7 + CellStart int `json:"cell_start"`
8 + CellEnd int `json:"cell_end"`
9 + Lecture string `json:"lecture"`
10 + Professor string `json:"professor"`
11 +}
1 +package models
2 +
3 +type Response struct {
4 + Status int `json:"status"`
5 + Message string `json:"message"`
6 + Data interface{} `json:"data"`
7 +}