freckie

Add: first commit

package main
import (
"encoding/json"
"io/ioutil"
)
type Config struct {
Server ConfigServer `json:"server"`
Database ConfigDatabase `json:"database"`
}
type ConfigServer struct {
LocalMode bool `json:"local_mode"`
Port int `json:"port"`
}
type ConfigDatabase struct {
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
Schema string `json:"schema"`
}
func LoadConfig(filePath string) (*Config, error) {
cfg := &Config{}
dataBytes, err := ioutil.ReadFile(filePath)
if err != nil {
return cfg, err
}
json.Unmarshal(dataBytes, cfg)
return cfg, nil
}
package endpoints
import (
"classroom/functions"
"classroom/models"
"database/sql"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/julienschmidt/httprouter"
)
// GET /timetables/<file_id>/<sheet_id>/cell
func (e *Endpoints) CellGet(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
if _email, ok := r.Header["X-User-Email"]; ok {
email = _email[0]
} else {
functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
return
}
// Get Path Parameters
fileID := ps.ByName("file_id")
sheetID := ps.ByName("sheet_id")
// Get Query Parameters
qp := r.URL.Query()
var cellColumn string
var cellStart int
var cellEnd int
var err error
if _cellColumn, ok := qp["column"]; ok {
cellColumn = strings.ToUpper(_cellColumn[0])
} else {
functions.ResponseError(w, 400, "column 파라미터를 보내세요.")
return
}
if _cellStart, ok := qp["start"]; ok {
cellStart, err = strconv.Atoi(_cellStart[0])
if err != nil {
functions.ResponseError(w, 400, "start 파라미터를 보내세요.")
return
}
}
if _cellEnd, ok := qp["end"]; ok {
cellEnd, err = strconv.Atoi(_cellEnd[0])
if err != nil {
functions.ResponseError(w, 400, "end 파라미터를 보내세요.")
return
}
}
// Check Permission
var _timetable, _email string
timetable := fmt.Sprintf("%s,%s", fileID, sheetID)
row := e.DB.QueryRow(`
SELECT a.timetable_id, u.email
FROM allowlist AS a, users AS u
WHERE a.timetable_id=?
AND a.user_id=u.id;
`, timetable)
if err := row.Scan(&_timetable, &_email); err != nil {
if err == sql.ErrNoRows {
functions.ResponseError(w, 404, "존재하지 않은 timetable.")
return
}
}
if _email != email {
functions.ResponseError(w, 403, "timetable 접근 권한 부족")
return
}
// Result Resp
resp := models.CellGetResponse{}
resp.Cells = []models.CellItem{}
// Querying
rows, err := e.DB.Query(`
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
FROM transactions AS t, users AS u
WHERE t.user_id=u.id
AND u.email=?
AND t.transaction_type=1
AND t.timetable_id=?
AND t.cell_column=?;`, email, timetable, cellColumn)
if err != nil {
if err == sql.ErrNoRows {
resp.CellsCount = 0
functions.ResponseOK(w, "success", resp)
return
}
functions.ResponseError(w, 500, err.Error())
return
}
defer rows.Close()
cells := []models.CellTransactionModel{}
for rows.Next() {
temp := models.CellTransactionModel{}
err := rows.Scan(&temp.UserEmail, &temp.UserID, &temp.CellColumn, &temp.CellStart, &temp.CellEnd, &temp.Lecture, &temp.Professor, &temp.TransactionID, &temp.CreatedAt, &temp.Capacity)
if err != nil {
continue
}
cells = append(cells, temp)
}
// Compare
for i := cellStart; i <= cellEnd; i++ {
isInRange := false
for _, cell := range cells {
if functions.InRange(i, cell.CellStart, cell.CellEnd) {
temp := models.CellItem{}
temp.Cell = fmt.Sprintf("%s%d", cellColumn, i)
temp.IsReserved = true
temp.UserEmail = cell.UserEmail
temp.UserID = cell.UserID
temp.Lecture = cell.Lecture
temp.Professor = cell.Professor
temp.TransactionID = cell.TransactionID
temp.CreatedAt = functions.ToKST(cell.CreatedAt)
temp.Capacity = cell.Capacity
resp.Cells = append(resp.Cells, temp)
isInRange = true
break
}
}
if !isInRange {
temp := models.CellItem{}
temp.Cell = fmt.Sprintf("%s%d", cellColumn, i)
temp.IsReserved = false
resp.Cells = append(resp.Cells, temp)
}
}
// Struct for response
resp.CellsCount = len(resp.Cells)
functions.ResponseOK(w, "success", resp)
}
package endpoints
import (
"database/sql"
)
type Endpoints struct {
DB *sql.DB
}
module classroom/endpoints
go 1.14
require (
classroom/functions v0.0.0
github.com/julienschmidt/httprouter v1.3.0
)
replace classroom/functions v0.0.0 => ../functions
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
package endpoints
import (
"classroom/functions"
"classroom/models"
"net/http"
"github.com/julienschmidt/httprouter"
)
// GET /
func (e *Endpoints) IndexGet(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.Header().Set("Content-Type", "application/json")
// Struct for response
resp := models.IndexResponse{}
resp.WelcomeMessage = "Hello, Kyung Hee!"
// Response with JSON
functions.ResponseOK(w, "success", resp)
}
package endpoints
import (
"classroom/functions"
"classroom/models"
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/julienschmidt/httprouter"
)
// POST /timetables/<file_id>/<sheet_id>/reservation
func (e *Endpoints) ReservationPost(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
if _email, ok := r.Header["X-User-Email"]; ok {
email = _email[0]
} else {
functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
return
}
// Get Path Parameters
fileID := ps.ByName("file_id")
sheetID := ps.ByName("sheet_id")
// Check Permission
var _timetable, _email string
var userID int
timetable := fmt.Sprintf("%s,%s", fileID, sheetID)
row := e.DB.QueryRow(`
SELECT a.timetable_id, u.email, u.id
FROM allowlist AS a, users AS u
WHERE a.timetable_id=?
AND a.user_id=u.id;
`, timetable)
if err := row.Scan(&_timetable, &_email, &userID); err != nil {
if err == sql.ErrNoRows {
functions.ResponseError(w, 404, "존재하지 않는 timetable")
return
}
functions.ResponseError(w, 500, "예기치 못한 에러 발생 : "+err.Error())
return
}
if _email != email {
functions.ResponseError(w, 403, "timetable 접근 권한 부족")
return
}
// Parse Request Data
type reqDataStruct struct {
Column *string `json:"column"`
Start *int `json:"start"`
End *int `json:"end"`
Lecture *string `json:"lecture"`
Professor *string `json:"professor"`
Capacity *int `json:"capacity"`
}
var reqData reqDataStruct
if strings.Contains(r.Header.Get("Content-Type"), "application/json") {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
functions.ResponseError(w, 500, err.Error())
}
json.Unmarshal(body, &reqData)
} else {
functions.ResponseError(w, 400, "JSON 형식만 가능합니다.")
return
}
if reqData.Column == nil || reqData.Start == nil || reqData.End == nil ||
reqData.Lecture == nil || reqData.Professor == nil || reqData.Capacity == nil {
functions.ResponseError(w, 400, "파라미터를 전부 보내주세요.")
return
}
// Querying (Cell Validation Check)
isPossible := true
rows, err := e.DB.Query(`
SELECT cell_start, cell_end
FROM transactions
WHERE transaction_type=1
AND cell_column=?;
`, *(reqData.Column))
if err == sql.ErrNoRows {
isPossible = true
}
defer rows.Close()
loopCheckingValidation:
for rows.Next() {
var _start, _end int
err = rows.Scan(&_start, &_end)
if err != nil {
continue
}
for i := *(reqData.Start); i <= *(reqData.End); i++ {
if functions.InRange(i, _start, _end) {
isPossible = false
break loopCheckingValidation
}
}
}
if !isPossible {
functions.ResponseError(w, 500, "해당 셀 범위에 예약이 존재합니다.")
return
}
// Result Resp
resp := models.ReservationPostResponse{}
// Querying (Making a Transaction)
res, err := e.DB.Exec(`
INSERT INTO transactions (transaction_type, user_id, timetable_id, lecture, capacity, cell_column, cell_start, cell_end, professor)
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?);
`, userID, timetable, *(reqData.Lecture), *(reqData.Capacity), *(reqData.Column), *(reqData.Start), *(reqData.End), *(reqData.Professor))
if err != nil {
functions.ResponseError(w, 500, err.Error())
return
}
resp.TransactionID, err = res.LastInsertId()
if err != nil {
functions.ResponseError(w, 500, err.Error())
return
}
resp.IsSuccess = true
resp.CellColumn = *(reqData.Column)
resp.CellStart = *(reqData.Start)
resp.CellEnd = *(reqData.End)
resp.Lecture = *(reqData.Lecture)
resp.Professor = *(reqData.Professor)
functions.ResponseOK(w, "success", resp)
}
// DELETE /timetables/<file_id>/<sheet_id>/reservation/<reservation_id>
func (e *Endpoints) ReservationDelete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get user email
var email string
if _email, ok := r.Header["X-User-Email"]; ok {
email = _email[0]
} else {
functions.ResponseError(w, 401, "X-User-Email 헤더를 보내세요.")
return
}
// Get Path Parameters
fileID := ps.ByName("file_id")
sheetID := ps.ByName("sheet_id")
reservationID := ps.ByName("reservation_id")
// Check Timetable Permission
var _timetable, _email string
var userID int
timetable := fmt.Sprintf("%s,%s", fileID, sheetID)
row := e.DB.QueryRow(`
SELECT a.timetable_id, u.email, u.id
FROM allowlist AS a, users AS u
WHERE a.timetable_id=?
AND a.user_id=u.id;
`, timetable)
if err := row.Scan(&_timetable, &_email, &userID); err != nil {
if err == sql.ErrNoRows {
functions.ResponseError(w, 404, "존재하지 않는 timetable")
return
}
functions.ResponseError(w, 500, "예기치 못한 에러 발생 : "+err.Error())
return
}
if _email != email {
functions.ResponseError(w, 403, "timetable 접근 권한 부족")
return
}
// Check Transaction Permission
var _transactionType int64
row = e.DB.QueryRow(`
SELECT u.email, t.transaction_type
FROM transactions AS t, users AS u
WHERE t.user_id=u.id
AND t.transaction_id=?;
`, reservationID)
err := row.Scan(&_email, &_transactionType)
if err != nil {
if err == sql.ErrNoRows {
functions.ResponseError(w, 404, "존재하지 않는 예약")
return
}
functions.ResponseError(w, 500, "예기치 못한 에러 발생 : "+err.Error())
return
}
if _email != email {
functions.ResponseError(w, 403, "예약 접근 권한 부족")
return
}
if _transactionType == 0 {
functions.ResponseError(w, 500, "이미 취소된 예약")
return
}
// Querying
res, err := e.DB.Exec(`
UPDATE transactions SET transaction_type=0 WHERE transaction_id=?
`, reservationID)
if err != nil {
functions.ResponseError(w, 500, err.Error())
return
}
if affected, _ := res.RowsAffected(); affected != 1 {
functions.ResponseError(w, 500, "예기치 못한 에러 발생. (RowsAffected != 1)")
return
}
functions.ResponseOK(w, "success", nil)
}
package functions
// InRange returns if the target is in the `range (start, end)
func InRange(target, start, end int) bool {
return (start <= target) && (target <= end)
}
module classroom/functions
go 1.14
package functions
import (
"encoding/json"
"log"
"net/http"
"classroom/models"
)
// ResponseOK make 200 response.
func ResponseOK(w http.ResponseWriter, msg string, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
w.WriteHeader(200)
json.NewEncoder(w).Encode(&models.Response{
Status: 200,
Message: msg,
Data: data,
})
}
// ResponseError make error response.
func ResponseError(w http.ResponseWriter, errorCode int, errorMsg string) {
log.Println("[Error] :", errorCode, errorMsg)
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
w.WriteHeader(errorCode)
json.NewEncoder(w).Encode(&models.Response{
Status: errorCode,
Message: errorMsg,
Data: nil,
})
}
package functions
import (
"time"
)
func ToKST(utc string) string {
t, _ := time.Parse(time.RFC3339, utc)
t = t.Add(9 * time.Hour)
return t.Format("2006-01-02 15:04:05")
}
module classroom
go 1.14
require (
github.com/go-sql-driver/mysql v1.5.0
github.com/julienschmidt/httprouter v1.3.0
github.com/rs/cors v1.7.0
classroom/endpoints v0.0.0
classroom/models v0.0.0
classroom/functions v0.0.0
)
replace (
classroom/endpoints v0.0.0 => ./endpoints
classroom/models v0.0.0 => ./models
classroom/functions v0.0.0 => ./functions
)
\ No newline at end of file
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
"classroom/endpoints"
"classroom/functions"
_ "github.com/go-sql-driver/mysql"
"github.com/julienschmidt/httprouter"
"github.com/rs/cors"
)
var logger *log.Logger
type HostSwitch map[string]http.Handler
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler := hs[r.Host]; handler != nil {
ip := r.RemoteAddr
log.Println("[Req]", r.Method, r.URL, ip)
handler.ServeHTTP(w, r)
} else {
functions.ResponseError(w, 403, "Forbidden hostname : "+r.Host)
}
}
func main() {
// Logger
logger = log.New(os.Stdout, "LOG ", log.LstdFlags)
// Config
cfg, err := LoadConfig("config.json")
if err != nil {
log.Fatal(err)
}
// DB Setting
if !(cfg.Server.LocalMode) {
time.Sleep(time.Second * 10)
}
dbStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?allowNativePasswords=true&parseTime=true",
cfg.Database.User,
cfg.Database.Password,
cfg.Database.Host,
cfg.Database.Port,
cfg.Database.Schema)
db, err := sql.Open("mysql", dbStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
ep := endpoints.Endpoints{DB: db}
// Router Setting
router := httprouter.New()
router.GET("/api", ep.IndexGet)
router.GET("/api/timetables/:file_id/:sheet_id/cell", ep.CellGet)
router.POST("/api/timetables/:file_id/:sheet_id/reservation", ep.ReservationPost)
router.DELETE("/api/timetables/:file_id/:sheet_id/reservation/:reservation_id", ep.ReservationDelete)
// Local Mode
portStr := strconv.Itoa(cfg.Server.Port)
if cfg.Server.LocalMode {
handler := cors.AllowAll().Handler(router)
// Start Server in Local Mode
log.Println("[Local Mode] Starting HTTP API Server on port", portStr)
log.Fatal(http.ListenAndServe(":"+portStr, handler))
} else { // Release Mode
handler := cors.AllowAll().Handler(router)
hs := make(HostSwitch)
hs["web-api"] = handler
// Start Server
log.Println("[Release Mode] Starting HTTP API Server on port", portStr)
log.Fatal(http.ListenAndServe(":"+portStr, hs))
}
}
package models
type CellTransactionModel struct {
TransactionID int
UserID int
UserEmail string
Lecture string
CellColumn string
CellStart int
CellEnd int
Professor string
CreatedAt string
Capacity int
}
type CellGetResponse struct {
Cells []CellItem `json:"cells"`
CellsCount int `json:"cells_count"`
}
type CellItem struct {
Cell string `json:"cell"`
IsReserved bool `json:"is_reserved"`
UserEmail string `json:"user_email"`
UserID int `json:"user_id"`
Lecture string `json:"lecture"`
Professor string `json:"professor"`
Capacity int `json:"capacity"`
TransactionID int `json:"transaction_id"`
CreatedAt string `json:"created_at"`
}
module classroom/models
go 1.14
package models
type IndexResponse struct {
WelcomeMessage string `json:"welcome_message"`
}
package models
type ReservationPostResponse struct {
IsSuccess bool `json:"is_success"`
TransactionID int64 `json:"transaction_id"`
CellColumn string `json:"cell_column"`
CellStart int `json:"cell_start"`
CellEnd int `json:"cell_end"`
Lecture string `json:"lecture"`
Professor string `json:"professor"`
}
package models
type Response struct {
Status int `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data"`
}