├── .gitignore ├── dto ├── generate.go ├── response.go ├── favor.go ├── user.go ├── share.go ├── plan.go └── config.go ├── middlewares ├── middlewares.go └── auth.go ├── go.mod ├── server └── init.go ├── utils └── token.go ├── rest-server.conf ├── README.md ├── rpc ├── CSTIRPC │ ├── CSTIRpcServer.proto │ ├── CSTIRpcServer_grpc.pb.go │ └── CSTIRpcServer.pb.go └── RPCClient.go ├── db ├── init.go └── initDatabase.go ├── routers ├── routers.go ├── user.go ├── generate.go ├── favor.go ├── share.go ├── config.go └── plan.go ├── main.go ├── config └── config.go ├── api-doc.txt └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.vscode 3 | /.settings 4 | /class-schedule-to-icalendar-restserver -------------------------------------------------------------------------------- /dto/generate.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type GenerateByPlanTokenReq struct { 4 | Token string `form:"token" binding:"required"` 5 | } 6 | 7 | type GenerateByPlanShareReq struct { 8 | ShareID int64 `form:"shareId" binding:"required"` 9 | } 10 | 11 | type GenerateRes struct { 12 | Content string `json:"content" binding:"required"` 13 | } 14 | -------------------------------------------------------------------------------- /middlewares/middlewares.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | var ms []gin.HandlerFunc = make([]gin.HandlerFunc, 0, 4) 6 | 7 | // Init is used to initialzed middlewares with gin.Engine 8 | func Init(r *gin.Engine) { 9 | for _, m := range ms { 10 | r.Use(m) 11 | } 12 | } 13 | 14 | func registerMiddleware(m gin.HandlerFunc) { 15 | ms = append(ms, m) 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/leafee98/class-schedule-to-icalendar-restserver 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/go-sql-driver/mysql v1.5.0 8 | github.com/golang/protobuf v1.4.3 9 | github.com/google/uuid v1.2.0 10 | github.com/jmoiron/sqlx v1.3.1 11 | github.com/sirupsen/logrus v1.7.0 12 | google.golang.org/grpc v1.35.0 13 | google.golang.org/protobuf v1.25.0 14 | ) 15 | -------------------------------------------------------------------------------- /server/init.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/leafee98/class-schedule-to-icalendar-restserver/config" 6 | ) 7 | 8 | // Engine is gin Engine 9 | var Engine *gin.Engine 10 | 11 | // Init create the gin engine 12 | func Init() { 13 | Engine = gin.Default() 14 | } 15 | 16 | // Run start listen and handle http request, call this func at last 17 | func Run() { 18 | Engine.Run(config.RestEndpoint) 19 | } 20 | -------------------------------------------------------------------------------- /utils/token.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | // GenerateToken will return random uuid removed dashes 10 | func GenerateToken() string { 11 | u, _ := uuid.NewRandom() 12 | var uuid string = u.String() 13 | 14 | // remove dashes in token 15 | var builder strings.Builder 16 | builder.Grow(len(uuid)) 17 | for _, c := range uuid { 18 | if c != '-' { 19 | builder.WriteRune(c) 20 | } 21 | } 22 | return builder.String() 23 | } 24 | -------------------------------------------------------------------------------- /rest-server.conf: -------------------------------------------------------------------------------- 1 | # RPCTarget is the RPC server listen address and port, e: 127.0.0.1:8047 2 | rpc-target = 127.0.0.1:8047 3 | 4 | # RestEndpoint is the endpoint of rest server's IP address, e: 0.0.0.0 5 | rest-endpoint = 0.0.0.0:8049 6 | 7 | # DatabaseSource is the endpoint of mariadb/mysql, e: user:pass@127.0.0.1:3306/db 8 | database-username = u_csti 9 | database-password = csti_pass 10 | database-host = 127.0.0.1:3306 11 | database-name = csti 12 | 13 | # HTTPBasepath is the base path while request this rest server, e: /api/ 14 | http-basepath = /api 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # class-schedule-to-icalendar-restserver 2 | 3 | the rest api server of class-schedule-to-icalendar, depend on [rest-api-grpc](github.com/leafee98/class-schedule-to-icalendar-rpcserver), require mariadb. 4 | 5 | ## install 6 | 7 | ### database configuration 8 | 9 | enable event_schedular in maridb to allow auto delete expired token in database. see also on [here](https://mariadb.com/docs/reference/mdb/system-variables/event_scheduler/) 10 | 11 | enable event_schedular in runtime. 12 | 13 | ``` 14 | https://mariadb.com/docs/reference/mdb/system-variables/event_scheduler/ 15 | ``` 16 | 17 | enable event_schedular in configuraton file. 18 | 19 | ``` 20 | [mariadb] 21 | event_scheduler=ON 22 | ``` -------------------------------------------------------------------------------- /rpc/CSTIRPC/CSTIRpcServer.proto: -------------------------------------------------------------------------------- 1 | 2 | syntax = "proto3"; 3 | 4 | option go_package = "github.com/leafee98/class-schedule-to-icalendar-restserver/rpc/CSTIRPC"; 5 | option java_multiple_files = true; 6 | option java_package = "com.github.leafee98.CSTI.rpcserver.rpc"; 7 | option java_outer_classname = "CSTIRpcServerProto"; 8 | 9 | package rpcserver; 10 | 11 | service CSTIRpcServer { 12 | // generate ical with json configure 13 | rpc jsonGenerate(ConfJson) returns (ResultIcal) {} 14 | rpc tomlGenerate(ConfToml) returns (ResultIcal) {} 15 | } 16 | 17 | message ConfJson { 18 | string content = 1; 19 | } 20 | 21 | message ConfToml { 22 | string content = 1; 23 | } 24 | 25 | message ResultIcal { 26 | string content = 2; 27 | } 28 | -------------------------------------------------------------------------------- /db/init.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jmoiron/sqlx" 7 | 8 | // for the side effect of driver register 9 | _ "github.com/go-sql-driver/mysql" 10 | "github.com/leafee98/class-schedule-to-icalendar-restserver/config" 11 | ) 12 | 13 | // DB is database object to do query 14 | var DB *sqlx.DB 15 | 16 | // Init initialize the db, make it usable 17 | func Init() error { 18 | var err error 19 | 20 | // auto parse database type datetime as []uint8 in go to time.Time 21 | DB, err = sqlx.Connect("mysql", dsn(config.DatabaseUsername, config.DatabasePassword, 22 | config.DatabaseHost, config.DatabaseName)) 23 | return err 24 | } 25 | 26 | func dsn(username, password, host, name string) string { 27 | return fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", username, password, host, name) 28 | } 29 | -------------------------------------------------------------------------------- /rpc/RPCClient.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/leafee98/class-schedule-to-icalendar-restserver/config" 8 | "github.com/leafee98/class-schedule-to-icalendar-restserver/rpc/CSTIRPC" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | var client CSTIRPC.CSTIRpcServerClient 13 | 14 | // JSONGenerate generate the icalendar result, request string and return string and error 15 | func JSONGenerate(content string) (string, error) { 16 | res, err := client.JsonGenerate(context.TODO(), &CSTIRPC.ConfJson{Content: content}) 17 | return res.GetContent(), err 18 | } 19 | 20 | // Init initialize RPCClient object to use rpc of rpcserver 21 | // try to connect to rpc server in 5 seconds 22 | func Init() error { 23 | conn, err := grpc.Dial(config.RPCTarget, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithTimeout(5*time.Second)) 24 | if err != nil { 25 | return err 26 | } 27 | client = CSTIRPC.NewCSTIRpcServerClient(conn) 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /dto/response.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | // Response is the outer json object used in response. 6 | // The response data should be added in the Data field 7 | type Response struct { 8 | Status string `json:"status"` 9 | Data interface{} `json:"data"` 10 | Time int64 `json:"time"` 11 | } 12 | 13 | // NewResponseFine will create a new Response with a "ok" as Status 14 | func NewResponseFine(data interface{}) Response { 15 | return Response{ 16 | Status: "ok", 17 | Time: time.Now().UnixNano() / int64(time.Millisecond) / int64(time.Nanosecond), 18 | Data: data, 19 | } 20 | } 21 | 22 | // NewResponseBad will create a new Response with a "bad" as Status, 23 | // mostly there should be a string work as a message in data field 24 | func NewResponseBad(data interface{}) Response { 25 | return Response{ 26 | Status: "bad", 27 | Time: time.Now().UnixNano() / int64(time.Millisecond) / int64(time.Nanosecond), 28 | Data: data, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /routers/routers.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/leafee98/class-schedule-to-icalendar-restserver/config" 6 | ) 7 | 8 | // Router type alias function to handle some request 9 | type Router struct { 10 | path string 11 | method string 12 | f func(*gin.Context) 13 | } 14 | 15 | var routers = make([]Router, 0) 16 | 17 | // RegisterRouter function store the router to register to Gin temporarily 18 | func RegisterRouter(path string, method string, f func(*gin.Context)) { 19 | routers = append(routers, Router{path: path, method: method, f: f}) 20 | } 21 | 22 | // Init register all stored router to Gin 23 | // This request a *gin.Engine initialized in server package 24 | func Init(engine *gin.Engine) error { 25 | routerGroup := engine.Group(config.HTTPBasepath) 26 | for _, r := range routers { 27 | switch r.method { 28 | case "get": 29 | routerGroup.GET(r.path, r.f) 30 | case "post": 31 | routerGroup.POST(r.path, r.f) 32 | } 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/leafee98/class-schedule-to-icalendar-restserver/config" 5 | "github.com/leafee98/class-schedule-to-icalendar-restserver/db" 6 | "github.com/leafee98/class-schedule-to-icalendar-restserver/middlewares" 7 | "github.com/leafee98/class-schedule-to-icalendar-restserver/routers" 8 | "github.com/leafee98/class-schedule-to-icalendar-restserver/rpc" 9 | "github.com/leafee98/class-schedule-to-icalendar-restserver/server" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func main() { 14 | var err error 15 | 16 | config.ParseParameter() 17 | if config.ConfigFile != "" { 18 | if err = config.LoadConfigFromFile(config.ConfigFile); err != nil { 19 | logrus.Fatal(err) 20 | } 21 | } 22 | 23 | config.LogCurrentConfig() 24 | 25 | if err = config.ValidParamCombination(); err != nil { 26 | logrus.Fatal(err) 27 | } 28 | 29 | // just initialize database or start server 30 | if config.InitDatabase { 31 | if err = db.InitDatabase(); err != nil { 32 | logrus.Fatal(err) 33 | } 34 | logrus.Info("database initialized") 35 | return 36 | } else { 37 | if err = db.Init(); err != nil { 38 | logrus.Fatalf("failed to connect to database. detail: %s", err.Error()) 39 | } else { 40 | logrus.Info("database connected") 41 | } 42 | } 43 | 44 | err = rpc.Init() 45 | if err != nil { 46 | logrus.Fatalf("Could not establish connection to %s. Please check the PRC server status. detail: %s", 47 | config.RPCTarget, err.Error()) 48 | } else { 49 | logrus.Info("rpc server connected") 50 | } 51 | 52 | logrus.Info("starting rest server...") 53 | 54 | // keep this initialize order! 55 | server.Init() 56 | middlewares.Init(server.Engine) 57 | routers.Init(server.Engine) 58 | server.Run() 59 | } 60 | -------------------------------------------------------------------------------- /dto/favor.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | type FavorConfigAddReq struct { 6 | ID int64 `json:"id" binding:"required"` 7 | } 8 | type FavorConfigAddRes string 9 | 10 | type FavorConfigRemoveReq struct { 11 | ID int64 `json:"id" binding:"required"` 12 | } 13 | type FavorConfigRemoveRes string 14 | 15 | type FavorConfigGetListReq struct { 16 | Offset int64 `json:"offset"` 17 | Count int64 `json:"count" binding:"required"` 18 | } 19 | 20 | type FavorConfigGetListRes struct { 21 | Configs []FavorConfigSummary `json:"configs" binding:"required"` 22 | } 23 | 24 | type FavorPlanAddReq struct { 25 | ID int64 `json:"id" binding:"required"` 26 | } 27 | type FavorPlanAddRes string 28 | 29 | type FavorPlanRemoveReq struct { 30 | ID int64 `json:"id" binding:"required"` 31 | } 32 | type FavorPlanRemoveRes string 33 | 34 | type FavorPlanGetListReq struct { 35 | Offset int64 `json:"offset"` 36 | Count int64 `json:"count" binding:"required"` 37 | } 38 | 39 | type FavorPlanGetListRes struct { 40 | Plans []FavorPlanSummary `json:"plans"` 41 | } 42 | 43 | ///////////////////////////////////// 44 | ////////// Utility ////////////////// 45 | ///////////////////////////////////// 46 | 47 | type FavorConfigSummary struct { 48 | ShareID int64 `json:"shareId" binding:"required"` 49 | Type int8 `json:"type" binding:"required"` 50 | Name string `json:"name" binding:"required"` 51 | Format int8 `json:"format" binding:"required"` 52 | Remark string `json:"remark" binding:"required"` 53 | FavorTime time.Time `json:"favorTime" binding:"required"` 54 | CreateTime time.Time `json:"createTime" binding:"required"` 55 | ModifyTime time.Time `json:"modifyTime" binding:"required"` 56 | } 57 | 58 | type FavorPlanSummary struct { 59 | ShareID int64 `json:"shareId" binding:"required"` 60 | Name string `json:"name" binding:"required"` 61 | Remark string `json:"remark" binding:"required"` 62 | FavorTime time.Time `json:"favorTime" binding:"required"` 63 | CreateTime time.Time `json:"createTime" binding:"required"` 64 | ModifyTime time.Time `json:"modifyTime" binding:"required"` 65 | } 66 | -------------------------------------------------------------------------------- /middlewares/auth.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/leafee98/class-schedule-to-icalendar-restserver/db" 8 | "github.com/leafee98/class-schedule-to-icalendar-restserver/utils" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // KeyStruct is the words used as keys setted in gin.Context 13 | type KeyStruct struct { 14 | UserID string 15 | } 16 | 17 | // Key stored the key values setted in gin.Context 18 | var Key KeyStruct 19 | 20 | func init() { 21 | Key = KeyStruct{ 22 | UserID: "userID", 23 | } 24 | 25 | registerMiddleware(verifyUser) 26 | } 27 | 28 | // add Key.UserID's valuein gin.Context base on request's token 29 | func verifyUser(c *gin.Context) { 30 | v, err := c.Cookie("token") 31 | if err == nil { 32 | var userID int64 33 | row := db.DB.QueryRow("select c_user_id from t_login_token where c_token = ?", v) 34 | err := row.Scan(&userID) 35 | 36 | if err == nil { 37 | c.Set(Key.UserID, userID) 38 | logrus.Infof("userID=%v verified", userID) 39 | return 40 | } else if err != sql.ErrNoRows { 41 | logrus.Error(err.Error()) 42 | } 43 | } 44 | logrus.Info("unauthorized visitor") 45 | } 46 | 47 | // RegisterToken will add an random token and userId in userTokenMap, and return the token, 48 | // so the middleware could add userId info from token to gin.Context per HTTP request later. 49 | // the token will be uuid string compatiable with rfc4122 removed dashes. 50 | // 51 | // duartion in hours 52 | // 53 | // Register new token will remove the older token. 54 | func RegisterToken(userID int64, duration int) string { 55 | token := utils.GenerateToken() 56 | 57 | // remove the older token 58 | _, err := db.DB.Exec("delete from t_login_token where c_user_id = ?", userID) 59 | if err != nil { 60 | logrus.Error(err.Error()) 61 | } 62 | 63 | // insert new token into database 64 | _, err = db.DB.Exec("insert into t_login_token (c_user_id, c_token, c_expire_time) values"+ 65 | " (?, ?, now() + interval ? day)", 66 | userID, token, duration) 67 | if err != nil { 68 | logrus.Error(err.Error()) 69 | } 70 | 71 | return token 72 | } 73 | 74 | // ExpireToken will remove the token in userTokenMap 75 | func ExpireToken(token string) { 76 | db.DB.Exec("delete from t_login_token where c_token = ?", token) 77 | } 78 | -------------------------------------------------------------------------------- /dto/user.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "crypto/sha256" 4 | 5 | // Full Database Properties 6 | // ID int64 `db:"c_id" json:"id" binding:"required"` 7 | // Email string `db:"c_email" json:"email" binding:"required"` 8 | // Nickname string `db:"c_nickname" json:"nickname" binding:"required"` 9 | // Username string `db:"c_username" json:"username" binding:"required"` 10 | // Password []byte `db:"c_password"` 11 | // Bio string `db:"c_bio" json:"bio" binding:"required"` 12 | // JoinTime time.Time `db:"c_join_time" json:"joinTime" binding:"required"` 13 | 14 | // Properties only in request 15 | // PasswordPlain string `json:"password" binding:"required"` 16 | 17 | // UserRegisterReq is used when requesting to register a new user 18 | type UserRegisterReq struct { 19 | Username string `db:"c_username" json:"username" binding:"required"` 20 | Password []byte `db:"c_password"` 21 | PasswordPlain string `json:"password" binding:"required"` 22 | Nickname string `db:"c_nickname" json:"nickname" binding:"required"` 23 | Email string `db:"c_email" json:"email" binding:"required"` 24 | } 25 | 26 | // UserRegisterRes is the response of register user request 27 | type UserRegisterRes struct { 28 | ID int64 `db:"c_id" json:"id" binding:"required"` 29 | } 30 | 31 | // UserLoginReq is usd when requesting to login 32 | type UserLoginReq struct { 33 | Username string `db:"c_username" json:"username" binding:"required"` 34 | Password []byte `db:"c_password"` 35 | PasswordPlain string `json:"password" binding:"required"` 36 | TokenDuration int `json:"tokenDuration" binding:"required"` 37 | } 38 | 39 | // UserLoginRes is the response of login request 40 | type UserLoginRes struct { 41 | ID int64 `db:"c_id" json:"id" binding:"required"` 42 | } 43 | 44 | func (u *UserRegisterReq) fillPasswordHash() { 45 | if u.Password != nil { 46 | return 47 | } 48 | var hash [32]byte = sha256.Sum256([]byte(u.PasswordPlain)) 49 | var hashSlice []byte = make([]byte, 0, 32) 50 | copy(hashSlice, hash[:]) 51 | u.Password = hashSlice 52 | } 53 | 54 | func (u *UserLoginReq) fillPasswordHash() { 55 | if u.Password != nil { 56 | return 57 | } 58 | var hash [32]byte = sha256.Sum256([]byte(u.PasswordPlain)) 59 | var hashSlice []byte = make([]byte, 0, 32) 60 | copy(hashSlice, hash[:]) 61 | u.Password = hashSlice 62 | } 63 | -------------------------------------------------------------------------------- /dto/share.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | // ConfigSummary is used in http response 6 | type ConfigSummary struct { 7 | ID int64 `db:"c_id" json:"id" binding:"required"` 8 | Type int8 `db:"c_type" json:"type" binding:"required"` 9 | Name string `db:"c_name" json:"name" binding:"required"` 10 | Format int8 `db:"c_format" json:"format" binding:"required"` 11 | Remark string `db:"c_remark" json:"remark" binding:"required"` 12 | CreateTime time.Time `db:"c_create_time" json:"createTime" binding:"required"` 13 | ModifyTime time.Time `db:"c_modify_time" json:"modifyTime" binding:"required"` 14 | } 15 | 16 | // ConfigDetail is used in http response 17 | type ConfigDetail struct { 18 | ID int64 `db:"c_id" json:"id" binding:"required"` 19 | Type int8 `db:"c_type" json:"type" binding:"required"` 20 | Name string `db:"c_name" json:"name" binding:"required"` 21 | Format int8 `db:"c_format" json:"format" binding:"required"` 22 | Content string `db:"c_content" json:"content" binding:"required"` 23 | Remark string `db:"c_remark" json:"remark" binding:"required"` 24 | CreateTime time.Time `db:"c_create_time" json:"createTime" binding:"required"` 25 | ModifyTime time.Time `db:"c_modify_time" json:"modifyTime" binding:"required"` 26 | } 27 | 28 | type ConfigShareDetail struct { 29 | ID int64 `db:"c_id" json:"id" binding:"required"` 30 | Remark string `db:"c_remark" json:"remark" binding:"required"` 31 | CreateTime time.Time `db:"c_create_time" json:"createTime" binding:"required"` 32 | } 33 | 34 | type PlanTokenDetail struct { 35 | Token string `json:"token" binding:"required"` 36 | CreateTime time.Time `json:"createTime" binding:"required"` 37 | } 38 | 39 | type PlanSummary struct { 40 | ID int64 `db:"c_id" json:"id" binding:"required"` 41 | Name string `db:"c_name" json:"name" binding:"required"` 42 | Remark string `db:"c_remark" json:"remark" binding:"required"` 43 | CreateTime time.Time `db:"c_create_time" json:"createTime" binding:"required"` 44 | ModifyTime time.Time `db:"c_modify_time" json:"modifyTime" binding:"required"` 45 | } 46 | 47 | type PlanShareDetail struct { 48 | ID int64 `db:"c_id" json:"id" binding:"required"` 49 | Remark string `db:"c_remark" json:"remark" binding:"required"` 50 | CreateTime time.Time `db:"c_create_time" json:"createTime" binding:"required"` 51 | } 52 | -------------------------------------------------------------------------------- /routers/user.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "database/sql" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/leafee98/class-schedule-to-icalendar-restserver/db" 10 | "github.com/leafee98/class-schedule-to-icalendar-restserver/dto" 11 | "github.com/leafee98/class-schedule-to-icalendar-restserver/middlewares" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // contains routers relative to user's account 16 | 17 | // run automatically to register routers in this file 18 | func init() { 19 | RegisterRouter("/register", "post", register) 20 | RegisterRouter("/login", "post", login) 21 | RegisterRouter("/logout", "post", logout) 22 | RegisterRouter("/logout", "get", logout) 23 | } 24 | 25 | func register(c *gin.Context) { 26 | var req dto.UserRegisterReq 27 | if bindOrAbort(c, &req) != nil { 28 | return 29 | } 30 | 31 | var cnt int64 32 | err := db.DB.Get(&cnt, "select count(c_id) from t_user where c_username = ? or c_email = ?", 33 | req.Username, req.Email) 34 | if err != nil { 35 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad(err.Error())) 36 | logrus.Errorf("err while register: %v", err.Error()) 37 | return 38 | } 39 | if cnt > 0 { 40 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("duplicated username or email")) 41 | logrus.Warnf("err while register: %s", "duplicated username or email") 42 | return 43 | } 44 | 45 | // file the hash password 46 | var hashPassArr [32]byte = passwordHash(req.PasswordPlain) 47 | req.Password = hashPassArr[:] 48 | 49 | res, err := db.DB.Exec("insert into t_user (c_username, c_password, c_email, c_nickname) "+ 50 | "values (?, ?, ?, ?)", req.Username, req.Password, req.Email, req.Email) 51 | 52 | if err != nil { 53 | c.AbortWithStatusJSON(http.StatusInternalServerError, dto.NewResponseBad(err.Error())) 54 | logrus.Error(err) 55 | } 56 | 57 | id, err := res.LastInsertId() 58 | if err != nil { 59 | c.AbortWithStatusJSON(http.StatusInternalServerError, dto.NewResponseBad(err.Error())) 60 | logrus.Error(err) 61 | } else { 62 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.UserRegisterRes{ID: id})) 63 | } 64 | } 65 | 66 | func login(c *gin.Context) { 67 | var req dto.UserLoginReq 68 | if bindOrAbort(c, &req) != nil { 69 | return 70 | } 71 | 72 | var dbID int64 73 | var dbPassword [32]byte 74 | row := db.DB.QueryRow("select c_id, c_password from t_user where c_username = ?", req.Username) 75 | 76 | var tmp []byte 77 | switch err := row.Scan(&dbID, &tmp); err { 78 | case sql.ErrNoRows: 79 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("user not exists")) 80 | return 81 | case nil: 82 | // fine 83 | default: 84 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 85 | return 86 | } 87 | 88 | copy(dbPassword[:], tmp) 89 | 90 | var passCipher [32]byte = passwordHash(req.PasswordPlain) 91 | if passCipher == dbPassword { 92 | // logdin success, register token and set cookie 93 | token := middlewares.RegisterToken(dbID, req.TokenDuration) 94 | c.SetSameSite(http.SameSiteStrictMode) 95 | c.SetCookie("token", token, 3600*24*req.TokenDuration, "/", "", false, false) 96 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.UserLoginRes{ID: dbID})) 97 | } else { 98 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("password incorrect")) 99 | } 100 | } 101 | 102 | func logout(c *gin.Context) { 103 | token, err := c.Cookie("token") 104 | c.SetSameSite(http.SameSiteStrictMode) 105 | c.SetCookie("token", "000", -1, "/", "", false, false) 106 | if err != nil { 107 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("unauthorized logout is forbidden")) 108 | } 109 | middlewares.ExpireToken(token) 110 | } 111 | 112 | // passwordHash do sha256 as hash to avoid using plain text 113 | func passwordHash(p string) [32]byte { 114 | return sha256.Sum256([]byte(p)) 115 | } 116 | -------------------------------------------------------------------------------- /routers/generate.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/leafee98/class-schedule-to-icalendar-restserver/db" 11 | "github.com/leafee98/class-schedule-to-icalendar-restserver/dto" 12 | "github.com/leafee98/class-schedule-to-icalendar-restserver/rpc" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func init() { 17 | RegisterRouter("/generate-by-plan-token", "get", generateByPlanToken) 18 | RegisterRouter("/generate-by-plan-share", "get", generateByPlanShare) 19 | } 20 | 21 | // require the token in get request 22 | // check the existence of token 23 | // check the existence of plan 24 | // validate config share 25 | func generateByPlanToken(c *gin.Context) { 26 | var req dto.GenerateByPlanTokenReq 27 | if bindOrAbort(c, &req) != nil { 28 | return 29 | } 30 | 31 | const sqlGetPlanId string = ` 32 | select c_id from t_plan where c_deleted = false and c_id = ( 33 | select c_plan_id from t_plan_token where c_token = ?);` 34 | 35 | var planID int64 36 | row := db.DB.QueryRow(sqlGetPlanId, req.Token) 37 | err := row.Scan(&planID) 38 | if err == sql.ErrNoRows { 39 | // invalid token or deleted plan 40 | c.AbortWithStatus(http.StatusBadRequest) 41 | return 42 | } else if err != nil { 43 | logrus.Error(err) 44 | c.AbortWithStatusJSON(http.StatusBadRequest, err.Error()) 45 | return 46 | } 47 | 48 | generateFromPlanId(c, planID) 49 | } 50 | 51 | func generateByPlanShare(c *gin.Context) { 52 | const sqlGetPlanId string = `select c_plan_id from t_plan_share where c_deleted = false and c_id = ?;` 53 | 54 | var req dto.GenerateByPlanShareReq 55 | if bindOrAbort(c, &req) != nil { 56 | return 57 | } 58 | 59 | var planID int64 60 | row := db.DB.QueryRow(sqlGetPlanId, req.ShareID) 61 | err := row.Scan(&planID) 62 | if err == sql.ErrNoRows { 63 | // invalid token or deleted plan 64 | c.AbortWithStatus(http.StatusBadRequest) 65 | return 66 | } else if err != nil { 67 | logrus.Error(err) 68 | c.AbortWithStatusJSON(http.StatusBadRequest, err.Error()) 69 | return 70 | } 71 | 72 | generateFromPlanId(c, planID) 73 | } 74 | 75 | ////////////////////////////////////////// 76 | //////// Generation Utility ////////////// 77 | ////////////////////////////////////////// 78 | 79 | func generateFromPlanId(c *gin.Context, planID int64) { 80 | const sqlGetConfig string = ` 81 | select c_content, c_type, c_format 82 | from t_config 83 | where c_deleted = false 84 | and c_id in ( 85 | select c_config_id 86 | from t_plan_config_relation 87 | where c_plan_id = ? 88 | ) 89 | union all 90 | select c_content, c_type, c_format 91 | from t_config 92 | where c_deleted = false 93 | and c_id in ( 94 | select c_config_id 95 | from t_config_share 96 | where c_deleted = false 97 | and c_id in ( 98 | select c_config_share_id 99 | from t_plan_config_share_relation 100 | where c_plan_id = ? 101 | ) 102 | );` 103 | 104 | rows, err := db.DB.Query(sqlGetConfig, planID, planID) 105 | defer rows.Close() 106 | if err != nil { 107 | logrus.Error(err) 108 | c.AbortWithStatusJSON(http.StatusBadRequest, err.Error()) 109 | return 110 | } 111 | 112 | const configJSONFormat string = `{ "global": %s, "lessons": [ %s ] }` 113 | var configGlobal string 114 | var configLessons []string = make([]string, 0) 115 | for rows.Next() { 116 | var content string 117 | var globalOrLesson, format int8 118 | err := rows.Scan(&content, &globalOrLesson, &format) 119 | if err != nil { 120 | logrus.Error(err) 121 | c.AbortWithStatusJSON(http.StatusBadRequest, err.Error()) 122 | return 123 | } 124 | 125 | if globalOrLesson == 1 { 126 | configGlobal = content 127 | } else if globalOrLesson == 2 { 128 | configLessons = append(configLessons, content) 129 | } 130 | } 131 | 132 | var configLessonsStr strings.Builder 133 | for i, s := range configLessons { 134 | if i != 0 { 135 | configLessonsStr.WriteString(",") 136 | } 137 | configLessonsStr.WriteString(s) 138 | } 139 | 140 | var configJSONRes string = fmt.Sprintf(configJSONFormat, configGlobal, configLessonsStr.String()) 141 | 142 | generateRes, err := rpc.JSONGenerate(configJSONRes) 143 | if err != nil { 144 | logrus.Error(err) 145 | c.AbortWithStatusJSON(http.StatusBadRequest, err.Error()) 146 | return 147 | } 148 | 149 | c.String(http.StatusOK, generateRes) 150 | } 151 | -------------------------------------------------------------------------------- /dto/plan.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | // Name string `json:"name" binding:"required"` 6 | // Remark string `json:"remark" binding:"required"` 7 | // ID int64 `json:"id" binding:"required"` 8 | // PlanID int64 `json:"planId" binding:"required"` 9 | // ConfigID int64 `json:"configId" binding:"required"` 10 | 11 | type PlanCreateReq struct { 12 | Name string `json:"name" binding:"required"` 13 | Remark string `json:"remark" binding:"required"` 14 | } 15 | 16 | type PlanCreateRes struct { 17 | ID int64 `json:"id" binding:"required"` 18 | } 19 | 20 | type PlanAddConfigReq struct { 21 | PlanID int64 `json:"planId" binding:"required"` 22 | ConfigID int64 `json:"configId" binding:"required"` 23 | } 24 | 25 | type PlanAddConfigRes string 26 | 27 | type PlanRemoveConfigReq struct { 28 | PlanID int64 `json:"planId" binding:"required"` 29 | ConfigID int64 `json:"configId" binding:"required"` 30 | } 31 | 32 | type PlanRemoveConfigRes string 33 | 34 | type PlanAddShareReq struct { 35 | PlanID int64 `json:"planId" binding:"required"` 36 | ConfigShareID int64 `json:"configShareId" binding:"required"` 37 | } 38 | 39 | type PlanAddShareRes string 40 | 41 | type PlanRemoveShareReq struct { 42 | PlanID int64 `json:"planId" binding:"required"` 43 | ConfigShareID int64 `json:"configShareId" binding:"required"` 44 | } 45 | 46 | type PlanRemoveShareRes string 47 | 48 | type PlanGetByIdReq struct { 49 | ID int64 `json:"id" binding:"required"` 50 | } 51 | 52 | type PlanGetByShareReq struct { 53 | ID int64 `json:"id" binding:"required"` 54 | } 55 | 56 | // response of PlanGetBy.. request 57 | type PlanGetRes struct { 58 | Name string `json:"name" binding:"required"` 59 | Remark string `json:"remark" binding:"required"` 60 | ID int64 `json:"id" binding:"required"` 61 | CreateTime time.Time `json:"createTime" binding:"required"` 62 | ModifyTime time.Time `json:"modifyTime" binding:"required"` 63 | Configs []ConfigDetail `json:"configs" binding:"required"` 64 | 65 | // the share's detail is the same as configs, but its ID is shareID, not configID 66 | Shares []ConfigDetail `json:"shares" binding:"required"` 67 | } 68 | 69 | type PlanRemoveReq struct { 70 | ID int64 `json:"id" binding:"required"` 71 | } 72 | 73 | type PlanRemoveRes string 74 | 75 | type PlanModifyReq struct { 76 | ID int64 `json:"id" binding:"required"` 77 | Name string `json:"name" binding:"required"` 78 | Remark string `json:"remark" binding:"required"` 79 | } 80 | 81 | type PlanModifyRes string 82 | 83 | type PlanGetListReq struct { 84 | // available value: "createTime", "modifyTime", "name", "id" 85 | SortBy string `json:"sortBy" binding:"required"` 86 | 87 | // will make response's Count to zero when Offset is bigger than the number of config belongs to user 88 | Offset int64 `json:"offset"` 89 | 90 | // max 30 91 | Count int64 `json:"count" binding:"required"` 92 | } 93 | 94 | type PlanGetListRes struct { 95 | Count int64 `json:"count" binding:"required"` 96 | Plans []PlanSummary `json:"plans" binding:"required"` 97 | } 98 | 99 | type PlanCreateTokenReq struct { 100 | ID int64 `json:"id" binding:"required"` 101 | } 102 | 103 | type PlanCreateTokenRes struct { 104 | Token string `json:"token" binding:"required"` 105 | } 106 | 107 | type PlanRevokeTokenReq struct { 108 | Token string `json:"token" binding:"required"` 109 | } 110 | 111 | type PlanRevokeTokenRes string 112 | 113 | // PlanGetTokenListReq is used to get all tokens of a plan 114 | type PlanGetTokenListReq struct { 115 | ID int64 `db:"c_id" json:"id" binding:"required"` 116 | } 117 | 118 | // PlanGetTokenListRes is used to respond ConfigGetTokenListReq 119 | type PlanGetTokenListRes struct { 120 | Tokens []PlanTokenDetail `json:"tokens" binding:"required"` 121 | Count int64 `json:"count" binding:"required"` 122 | } 123 | 124 | type PlanShareCreateReq struct { 125 | ID int64 `db:"c_id" json:"id" binding:"required"` 126 | Remark string `db:"c_remark" json:"remark" binding:"required"` 127 | } 128 | 129 | type PlanShareCreateRes struct { 130 | ID int64 `db:"c_id" json:"id" binding:"required"` 131 | } 132 | 133 | type PlanShareModifyReq struct { 134 | ID int64 `db:"c_id" json:"id" binding:"required"` 135 | Remark string `db:"c_remark" json:"remark" binding:"required"` 136 | } 137 | 138 | type PlanShareModifyRes string 139 | 140 | type PlanShareRevokeReq struct { 141 | ID int64 `db:"c_id" json:"id" binding:"required"` 142 | } 143 | 144 | type PlanShareRevokeRes string 145 | 146 | type PlanShareGetListReq struct { 147 | ID int64 `db:"c_id" json:"id" binding:"required"` 148 | } 149 | 150 | type PlanShareGetListRes struct { 151 | Shares []PlanShareDetail `json:"shares" binding:"required"` 152 | } 153 | -------------------------------------------------------------------------------- /dto/config.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "time" 4 | 5 | // Full Database Properties 6 | // ID int64 `db:"c_id" json:"id" binding:"required"` 7 | // Type int8 `db:"c_type" json:"type" binding:"required"` 8 | // Name string `db:"c_name" json:"name" binding:"required"` 9 | // Content string `db:"c_content" json:"content" binding:"required"` 10 | // Format int8 `db:"c_format" json:"format" binding:"required"` 11 | // OwnerID int64 `db:"c_owner_id" json:"ownerId" binding:"required"` 12 | // Remark string `db:"c_remark" json:"remark" binding:"required"` 13 | // CreateTime time.Time `db:"c_create_time" json:"createTime" binding:"required"` 14 | // ModifyTime time.Time `db:"c_modify_time" json:"modifyTime" binding:"required"` 15 | // Deleted bool `db:"c_deleted" json:"deleted" binding:"required"` 16 | 17 | const ( 18 | LimitConfigFormatMin = 1 19 | LimitConfigFormatMax = 2 20 | LimitConfigTypeMin = 1 21 | LimitConfigTypeMax = 2 22 | ) 23 | 24 | // ConfigCreateReq is used in the request to create a new global config 25 | type ConfigCreateReq struct { 26 | Name string `db:"c_name" json:"name" binding:"required"` 27 | Type int8 `db:"c_type" json:"type" binding:"required"` 28 | Content string `db:"c_content" json:"content" binding:"required"` 29 | Format int8 `db:"c_format" json:"format" binding:"required"` 30 | Remark string `db:"c_remark" json:"remark" binding:"required"` 31 | } 32 | 33 | // ConfigCreateRes is used as response of ConfigCreateReq 34 | type ConfigCreateRes struct { 35 | ID int64 `db:"c_id" json:"id" binding:"required"` 36 | } 37 | 38 | // ConfigGetByIDReq is used to get config's info with it's ID 39 | type ConfigGetByIDReq struct { 40 | ID int64 `db:"c_id" json:"id" binding:"required"` 41 | } 42 | 43 | // ConfigGetByShareReq is used to get config's info with share link's ID 44 | type ConfigGetByShareReq struct { 45 | ID int64 `db:"c_id" json:"id" binding:"required"` 46 | } 47 | 48 | // ConfigGetRes is used as response as ConfigGetReq 49 | type ConfigGetRes struct { 50 | ID int64 `db:"c_id" json:"id" binding:"required"` 51 | Type int8 `db:"c_type" json:"type" binding:"required"` 52 | Name string `db:"c_name" json:"name" binding:"required"` 53 | Content string `db:"c_content" json:"content" binding:"required"` 54 | Format int8 `db:"c_format" json:"format" binding:"required"` 55 | Remark string `db:"c_remark" json:"remark" binding:"required"` 56 | CreateTime time.Time `db:"c_create_time" json:"createTime" binding:"required"` 57 | ModifyTime time.Time `db:"c_modify_time" json:"modifyTime" binding:"required"` 58 | } 59 | 60 | // ConfigModifyReq is used to modify a config. 61 | // Owner should not change the property Type. 62 | type ConfigModifyReq struct { 63 | ID int64 `db:"c_id" json:"id" binding:"required"` 64 | Name string `db:"c_name" json:"name" binding:"required"` 65 | Content string `db:"c_content" json:"content" binding:"required"` 66 | Format int8 `db:"c_format" json:"format" binding:"required"` 67 | Remark string `db:"c_remark" json:"remark" binding:"required"` 68 | } 69 | 70 | // return status, succeed is "ok" 71 | // Should be the same as Response.Status 72 | type ConfigModifyRes string 73 | 74 | // ConfigRemoveReq is used to remove config 75 | type ConfigRemoveReq struct { 76 | ID int64 `db:"c_id" json:"id" binding:"required"` 77 | } 78 | 79 | // return status, succeed is "ok" 80 | // Should be the same as Response.Status 81 | type ConfigRemoveRes string 82 | 83 | // ConfigGetListReq is used to get a list of user's config 84 | type ConfigGetListReq struct { 85 | // available value: "createTime", "modifyTime", "name", "id" 86 | SortBy string `json:"sortBy" binding:"required"` 87 | 88 | // will make response's Count to zero when Offset is bigger than the number of config belongs to user 89 | Offset int64 `json:"offset"` 90 | 91 | // max 30 92 | Count int64 `json:"count" binding:"required"` 93 | } 94 | 95 | // ConfigGetListRes respond to ConfigGetListReq 96 | type ConfigGetListRes struct { 97 | Count int64 `json:"count" binding:"required"` 98 | Configs []ConfigSummary `json:"configs" binding:"required"` 99 | } 100 | 101 | type ConfigShareCreateReq struct { 102 | ID int64 `db:"c_id" json:"id" binding:"required"` 103 | Remark string `db:"c_remark" json:"remark" binding:"required"` 104 | } 105 | 106 | type ConfigShareCreateRes struct { 107 | ID int64 `db:"c_id" json:"id" binding:"required"` 108 | } 109 | 110 | type ConfigShareModifyReq struct { 111 | ID int64 `db:"c_id" json:"id" binding:"required"` 112 | Remark string `db:"c_remark" json:"remark" binding:"required"` 113 | } 114 | 115 | type ConfigShareModifyRes string 116 | 117 | type ConfigShareRevokeReq struct { 118 | ID int64 `db:"c_id" json:"id" binding:"required"` 119 | } 120 | 121 | type ConfigShareRevokeRes string 122 | 123 | type ConfigShareGetListReq struct { 124 | ID int64 `db:"c_id" json:"id" binding:"required"` 125 | } 126 | 127 | type ConfigShareGetListRes struct { 128 | Shares []ConfigShareDetail `json:"shares" binding:"required"` 129 | } 130 | -------------------------------------------------------------------------------- /rpc/CSTIRPC/CSTIRpcServer_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package CSTIRPC 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // CSTIRpcServerClient is the client API for CSTIRpcServer service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type CSTIRpcServerClient interface { 21 | // generate ical with json configure 22 | JsonGenerate(ctx context.Context, in *ConfJson, opts ...grpc.CallOption) (*ResultIcal, error) 23 | TomlGenerate(ctx context.Context, in *ConfToml, opts ...grpc.CallOption) (*ResultIcal, error) 24 | } 25 | 26 | type cSTIRpcServerClient struct { 27 | cc grpc.ClientConnInterface 28 | } 29 | 30 | func NewCSTIRpcServerClient(cc grpc.ClientConnInterface) CSTIRpcServerClient { 31 | return &cSTIRpcServerClient{cc} 32 | } 33 | 34 | func (c *cSTIRpcServerClient) JsonGenerate(ctx context.Context, in *ConfJson, opts ...grpc.CallOption) (*ResultIcal, error) { 35 | out := new(ResultIcal) 36 | err := c.cc.Invoke(ctx, "/rpcserver.CSTIRpcServer/jsonGenerate", in, out, opts...) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return out, nil 41 | } 42 | 43 | func (c *cSTIRpcServerClient) TomlGenerate(ctx context.Context, in *ConfToml, opts ...grpc.CallOption) (*ResultIcal, error) { 44 | out := new(ResultIcal) 45 | err := c.cc.Invoke(ctx, "/rpcserver.CSTIRpcServer/tomlGenerate", in, out, opts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return out, nil 50 | } 51 | 52 | // CSTIRpcServerServer is the server API for CSTIRpcServer service. 53 | // All implementations must embed UnimplementedCSTIRpcServerServer 54 | // for forward compatibility 55 | type CSTIRpcServerServer interface { 56 | // generate ical with json configure 57 | JsonGenerate(context.Context, *ConfJson) (*ResultIcal, error) 58 | TomlGenerate(context.Context, *ConfToml) (*ResultIcal, error) 59 | mustEmbedUnimplementedCSTIRpcServerServer() 60 | } 61 | 62 | // UnimplementedCSTIRpcServerServer must be embedded to have forward compatible implementations. 63 | type UnimplementedCSTIRpcServerServer struct { 64 | } 65 | 66 | func (UnimplementedCSTIRpcServerServer) JsonGenerate(context.Context, *ConfJson) (*ResultIcal, error) { 67 | return nil, status.Errorf(codes.Unimplemented, "method JsonGenerate not implemented") 68 | } 69 | func (UnimplementedCSTIRpcServerServer) TomlGenerate(context.Context, *ConfToml) (*ResultIcal, error) { 70 | return nil, status.Errorf(codes.Unimplemented, "method TomlGenerate not implemented") 71 | } 72 | func (UnimplementedCSTIRpcServerServer) mustEmbedUnimplementedCSTIRpcServerServer() {} 73 | 74 | // UnsafeCSTIRpcServerServer may be embedded to opt out of forward compatibility for this service. 75 | // Use of this interface is not recommended, as added methods to CSTIRpcServerServer will 76 | // result in compilation errors. 77 | type UnsafeCSTIRpcServerServer interface { 78 | mustEmbedUnimplementedCSTIRpcServerServer() 79 | } 80 | 81 | func RegisterCSTIRpcServerServer(s grpc.ServiceRegistrar, srv CSTIRpcServerServer) { 82 | s.RegisterService(&CSTIRpcServer_ServiceDesc, srv) 83 | } 84 | 85 | func _CSTIRpcServer_JsonGenerate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 86 | in := new(ConfJson) 87 | if err := dec(in); err != nil { 88 | return nil, err 89 | } 90 | if interceptor == nil { 91 | return srv.(CSTIRpcServerServer).JsonGenerate(ctx, in) 92 | } 93 | info := &grpc.UnaryServerInfo{ 94 | Server: srv, 95 | FullMethod: "/rpcserver.CSTIRpcServer/jsonGenerate", 96 | } 97 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 98 | return srv.(CSTIRpcServerServer).JsonGenerate(ctx, req.(*ConfJson)) 99 | } 100 | return interceptor(ctx, in, info, handler) 101 | } 102 | 103 | func _CSTIRpcServer_TomlGenerate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 104 | in := new(ConfToml) 105 | if err := dec(in); err != nil { 106 | return nil, err 107 | } 108 | if interceptor == nil { 109 | return srv.(CSTIRpcServerServer).TomlGenerate(ctx, in) 110 | } 111 | info := &grpc.UnaryServerInfo{ 112 | Server: srv, 113 | FullMethod: "/rpcserver.CSTIRpcServer/tomlGenerate", 114 | } 115 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 116 | return srv.(CSTIRpcServerServer).TomlGenerate(ctx, req.(*ConfToml)) 117 | } 118 | return interceptor(ctx, in, info, handler) 119 | } 120 | 121 | // CSTIRpcServer_ServiceDesc is the grpc.ServiceDesc for CSTIRpcServer service. 122 | // It's only intended for direct use with grpc.RegisterService, 123 | // and not to be introspected or modified (even as a copy) 124 | var CSTIRpcServer_ServiceDesc = grpc.ServiceDesc{ 125 | ServiceName: "rpcserver.CSTIRpcServer", 126 | HandlerType: (*CSTIRpcServerServer)(nil), 127 | Methods: []grpc.MethodDesc{ 128 | { 129 | MethodName: "jsonGenerate", 130 | Handler: _CSTIRpcServer_JsonGenerate_Handler, 131 | }, 132 | { 133 | MethodName: "tomlGenerate", 134 | Handler: _CSTIRpcServer_TomlGenerate_Handler, 135 | }, 136 | }, 137 | Streams: []grpc.StreamDesc{}, 138 | Metadata: "CSTIRpcServer.proto", 139 | } 140 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | "strings" 11 | 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // RPCTarget is the RPC server listen address and port, e: 127.0.0.1:8047 16 | var RPCTarget string 17 | 18 | // RestEndpoint is the endpoint of rest server's IP address, e: 0.0.0.0 19 | var RestEndpoint string 20 | 21 | // Database connect info 22 | var DatabaseUsername string 23 | var DatabasePassword string 24 | var DatabaseHost string 25 | var DatabaseName string 26 | 27 | // HTTPBasepath is the base path while request this rest server, e: /api 28 | var HTTPBasepath string 29 | 30 | // if InitDatabase is set, just init database but don't start server 31 | var InitDatabase bool 32 | 33 | // specifiy the path of config file 34 | var ConfigFile string 35 | 36 | // const name of each configuration 37 | type paramNames struct { 38 | DatabaseUsername string 39 | DatabasePassword string 40 | DatabaseHost string 41 | DatabaseName string 42 | 43 | RPCTarget string 44 | RestEndpoint string 45 | HTTPBasepath string 46 | InitDatabase string 47 | ConfigFile string 48 | } 49 | 50 | var pn paramNames = paramNames{ 51 | DatabaseUsername: "database-username", 52 | DatabasePassword: "database-password", 53 | DatabaseHost: "database-host", 54 | DatabaseName: "database-name", 55 | 56 | RPCTarget: "rpc-target", 57 | RestEndpoint: "rest-endpoint", 58 | HTTPBasepath: "http-basepath", 59 | InitDatabase: "init-database", 60 | ConfigFile: "config", 61 | } 62 | 63 | // LoadConfig function load config from file whose path is confPath 64 | func LoadConfigFromFile(confPath string) error { 65 | file, err := os.Open(confPath) 66 | if err != nil { 67 | return err 68 | } 69 | defer file.Close() 70 | 71 | reader := bufio.NewReader(file) 72 | for { 73 | line, err := reader.ReadString('\n') 74 | if (len(line) == 0 || line[0] == '#') && err == nil { 75 | continue 76 | } 77 | 78 | if symbolEqual := strings.Index(line, "="); symbolEqual >= 0 { 79 | if key := strings.TrimSpace(line[:symbolEqual]); len(key) > 0 { 80 | value := "" 81 | if len(line) > symbolEqual { 82 | value = strings.TrimSpace(line[symbolEqual+1:]) 83 | } 84 | 85 | if err = loadSingleConfig(key, value); err != nil { 86 | return err 87 | } 88 | } 89 | } 90 | 91 | if err == io.EOF { 92 | break 93 | } else if err != nil { 94 | return err 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | func ParseParameter() { 101 | flag.StringVar(&DatabaseUsername, pn.DatabaseUsername, "", "username used to login database service.") 102 | flag.StringVar(&DatabasePassword, pn.DatabasePassword, "", "password used to login database service.") 103 | flag.StringVar(&DatabaseHost, pn.DatabaseHost, "", "host of database service. e: 127.0.0.1:3306") 104 | flag.StringVar(&DatabaseName, pn.DatabaseName, "", "name of database to use.") 105 | 106 | flag.StringVar(&RPCTarget, pn.RPCTarget, "", "RPCTarget is the RPC server listen address and port,"+ 107 | "e: 127.0.0.1:8047") 108 | flag.StringVar(&RestEndpoint, pn.RestEndpoint, "", "RestEndpoint is the endpoint of rest server's IP address,"+ 109 | "e: 0.0.0.0") 110 | flag.StringVar(&HTTPBasepath, pn.HTTPBasepath, "", "HTTPBasepath is the base path while request this rest server,"+ 111 | "e: /api") 112 | flag.BoolVar(&InitDatabase, pn.InitDatabase, false, "add this parameter to init database and don't start server."+ 113 | " (this parameter can noly specified in command line)") 114 | flag.StringVar(&ConfigFile, pn.ConfigFile, "", "specifiy the path of config file. "+ 115 | " (this parameter can noly specified in command line)") 116 | flag.Parse() 117 | } 118 | 119 | func loadSingleConfig(key, value string) error { 120 | switch key { 121 | case pn.RPCTarget: 122 | if RPCTarget == "" { 123 | RPCTarget = value 124 | } 125 | case pn.RestEndpoint: 126 | if RestEndpoint == "" { 127 | RestEndpoint = value 128 | } 129 | case pn.HTTPBasepath: 130 | if HTTPBasepath == "" { 131 | HTTPBasepath = value 132 | } 133 | 134 | case pn.DatabaseUsername: 135 | if DatabaseUsername == "" { 136 | DatabaseUsername = value 137 | } 138 | case pn.DatabasePassword: 139 | if DatabasePassword == "" { 140 | DatabasePassword = value 141 | } 142 | case pn.DatabaseHost: 143 | if DatabaseHost == "" { 144 | DatabaseHost = value 145 | } 146 | case pn.DatabaseName: 147 | if DatabaseName == "" { 148 | DatabaseName = value 149 | } 150 | default: 151 | return errors.New(fmt.Sprintf("unrecognized: %s = %s", key, value)) 152 | } 153 | return nil 154 | } 155 | 156 | func ValidParamCombination() error { 157 | if InitDatabase { 158 | if !validDatabaseSource() { 159 | return errors.New(fmt.Sprintf("when using %s, you must specify %s, %s, %s and %s", 160 | pn.InitDatabase, pn.DatabaseUsername, pn.DatabasePassword, pn.DatabaseHost, pn.DatabaseName)) 161 | } 162 | } else { 163 | if !validDatabaseSource() || 164 | HTTPBasepath == "" || 165 | RestEndpoint == "" || 166 | RPCTarget == "" { 167 | return errors.New("you haven't config all option") 168 | } 169 | } 170 | return nil 171 | } 172 | 173 | func validDatabaseSource() bool { 174 | return DatabaseUsername != "" && 175 | DatabasePassword != "" && 176 | DatabaseHost != "" && 177 | DatabaseName != "" 178 | } 179 | 180 | func LogCurrentConfig() { 181 | logrus.Info("========== current config ==========") 182 | logrus.Infof("%20s = %s", pn.ConfigFile, ConfigFile) 183 | logrus.Infof("%20s = %t", pn.InitDatabase, InitDatabase) 184 | 185 | logrus.Infof("%20s = %s", pn.DatabaseUsername, DatabaseUsername) 186 | logrus.Infof("%20s = %s", pn.DatabasePassword, strings.Repeat("*", len(DatabasePassword))) 187 | logrus.Infof("%20s = %s", pn.DatabaseHost, DatabaseHost) 188 | logrus.Infof("%20s = %s", pn.DatabaseName, DatabaseName) 189 | 190 | logrus.Infof("%20s = %s", pn.RPCTarget, RPCTarget) 191 | logrus.Infof("%20s = %s", pn.RestEndpoint, RestEndpoint) 192 | logrus.Infof("%20s = %s", pn.HTTPBasepath, HTTPBasepath) 193 | logrus.Info("======== current config end =========") 194 | } 195 | -------------------------------------------------------------------------------- /db/initDatabase.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | // for the side effect of driver register 8 | _ "github.com/go-sql-driver/mysql" 9 | "github.com/sirupsen/logrus" 10 | 11 | "github.com/jmoiron/sqlx" 12 | "github.com/leafee98/class-schedule-to-icalendar-restserver/config" 13 | ) 14 | 15 | const initSql = ` 16 | drop database if exists %s; 17 | create database %s character set='utf8mb4' collate='utf8mb4_unicode_ci'; 18 | 19 | use %s; 20 | create table t_user ( 21 | c_id integer primary key AUTO_INCREMENT, 22 | c_email varchar(64), 23 | c_nickname varchar(32), 24 | c_username varchar(32), 25 | c_password binary(32), 26 | c_bio varchar(300) default '', 27 | c_join_time datetime not null default now() 28 | ); 29 | 30 | create table t_config ( 31 | c_id integer primary key AUTO_INCREMENT, 32 | c_type tinyint, # 1-global, 2-lesson 33 | c_name varchar(64), 34 | c_content varchar(1024), 35 | c_format tinyint, # 1-json, 2-toml 36 | c_owner_id integer, 37 | c_remark varchar(300), 38 | c_create_time datetime not null default now(), # default create time is now() 39 | c_modify_time datetime not null default now(), # default modify time is now() 40 | c_deleted bool default false, 41 | 42 | constraint foreign key (c_owner_id) references t_user (c_id) 43 | ); 44 | 45 | create table t_config_share ( 46 | c_id integer primary key AUTO_INCREMENT, 47 | # c_access_uid varchar(64) unique, 48 | c_config_id integer, 49 | c_create_time datetime not null default now(), 50 | c_remark varchar(300), 51 | c_deleted bool default false, 52 | 53 | constraint foreign key (c_config_id) references t_config (c_id) 54 | ); 55 | 56 | create table t_user_favourite_config ( 57 | c_id integer primary key AUTO_INCREMENT, 58 | c_user_id integer, 59 | c_config_share_id integer, 60 | c_create_time datetime not null default now(), # default create time is now() 61 | 62 | constraint foreign key (c_user_id) references t_user (c_id), 63 | constraint foreign key (c_config_share_id) references t_config_share (c_id) 64 | ); 65 | 66 | create table t_plan ( 67 | c_id integer primary key AUTO_INCREMENT, 68 | c_name varchar(64), 69 | c_owner_id integer, 70 | c_remark varchar(300), 71 | c_create_time datetime not null default now(), # default create time is now() 72 | c_modify_time datetime not null default now(), # default modify time is now() 73 | c_deleted bool default false, 74 | 75 | constraint foreign key (c_owner_id) references t_user (c_id) 76 | ); 77 | 78 | create table t_plan_config_relation ( 79 | c_id integer primary key AUTO_INCREMENT, 80 | c_plan_id integer, 81 | c_config_id integer, 82 | 83 | constraint foreign key (c_plan_id) references t_plan (c_id), 84 | constraint foreign key (c_config_id) references t_config (c_id) 85 | ); 86 | 87 | create table t_plan_config_share_relation ( 88 | c_id integer primary key AUTO_INCREMENT, 89 | c_plan_id integer, 90 | c_config_share_id integer, 91 | 92 | constraint foreign key (c_plan_id) references t_plan (c_id), 93 | constraint foreign key (c_config_share_id) references t_config_share(c_id) 94 | ); 95 | 96 | create table t_plan_share ( 97 | c_id integer primary key AUTO_INCREMENT, 98 | # c_access_uid varchar(64) unique, # length of UUID 99 | c_plan_id integer, 100 | c_create_time datetime not null default now(), 101 | c_remark varchar(300), 102 | c_deleted bool default false, 103 | 104 | constraint foreign key (c_plan_id) references t_plan (c_id) 105 | ); 106 | 107 | create table t_user_favourite_plan ( 108 | c_id integer primary key AUTO_INCREMENT, 109 | c_user_id integer, 110 | c_plan_share_id integer, 111 | c_create_time datetime not null default now(), # default create time is now() 112 | 113 | constraint foreign key (c_user_id) references t_user (c_id), 114 | constraint foreign key (c_plan_share_id) references t_plan_share (c_id) 115 | ); 116 | 117 | create table t_login_token ( 118 | c_id integer primary key AUTO_INCREMENT, 119 | c_user_id integer, 120 | c_token varchar(32), # token will be uuid string removed dashes 121 | c_expire_time datetime not null default date_add(now(), interval 3 day), 122 | 123 | constraint foreign key (c_user_id) references t_user (c_id) 124 | ); 125 | 126 | create table t_plan_token ( 127 | c_id integer primary key AUTO_INCREMENT, 128 | c_token varchar(32) unique, # token will be uuid string removed dashes 129 | c_plan_id integer, 130 | c_create_time datetime not null default now(), 131 | 132 | constraint foreign key (c_plan_id) references t_plan (c_id) 133 | ); 134 | 135 | 136 | # auto delete expired token 137 | create event auto_remove_expired_token 138 | on schedule every 4 hour 139 | comment 'auto delete expired token' 140 | do 141 | delete from t_login_token where c_expire_time > now(); 142 | 143 | # update the c_modify_time to now() on table t_config 144 | create trigger t_config_update_modify_time 145 | before update on t_config 146 | for each row 147 | set new.c_modify_time = now(); 148 | 149 | # update the c_modify_time to now() on table t_plan 150 | create trigger t_plan_update_modify_time 151 | before update on t_plan 152 | for each row 153 | set new.c_modify_time = now(); 154 | 155 | # update the c_modify_time to now() on table t_plan when 156 | # 1. add config to plan 157 | # 2. remove config from plan 158 | # 3. add config shared to plan 159 | # 4. remove config shared from plan 160 | create trigger t_plan_update_modify_time_relationship_insert 161 | after insert on t_plan_config_relation 162 | for each row 163 | update t_plan set c_modify_time = now() where c_id = new.c_plan_id; 164 | create trigger t_plan_update_modify_time_relationship_delete 165 | after delete on t_plan_config_relation 166 | for each row 167 | update t_plan set c_modify_time = now() where c_id = old.c_plan_id; 168 | create trigger t_plan_update_modify_time_share_relationship_insert 169 | after insert on t_plan_config_share_relation 170 | for each row 171 | update t_plan set c_modify_time = now() where c_id = new.c_plan_id; 172 | create trigger t_plan_update_modify_time_share_relationship_delete 173 | after delete on t_plan_config_share_relation 174 | for each row 175 | update t_plan set c_modify_time = now() where c_id = old.c_plan_id; 176 | ` 177 | 178 | func getSqlCommand() string { 179 | return fmt.Sprintf(initSql, config.DatabaseName, config.DatabaseName, config.DatabaseName) 180 | } 181 | 182 | func InitDatabase() error { 183 | DB, err := sqlx.Connect("mysql", dsn(config.DatabaseUsername, config.DatabasePassword, config.DatabaseHost, "")) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | trans, err := DB.Begin() 189 | for _, command := range strings.Split(getSqlCommand(), ";") { 190 | if len(strings.Trim(command, " \t\n;")) == 0 { 191 | continue 192 | } 193 | if _, err = trans.Exec(command); err != nil { 194 | logrus.Info(command) 195 | return err 196 | } 197 | } 198 | err = trans.Commit() 199 | return err 200 | } 201 | -------------------------------------------------------------------------------- /routers/favor.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/leafee98/class-schedule-to-icalendar-restserver/db" 8 | "github.com/leafee98/class-schedule-to-icalendar-restserver/dto" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // run automatically to register routers in this file 13 | func init() { 14 | RegisterRouter("/favor-config-add", "post", favorConfigAdd) 15 | RegisterRouter("/favor-config-remove", "post", favorConfigRemove) 16 | RegisterRouter("/favor-config-get-list", "post", favorConfigGetList) 17 | 18 | RegisterRouter("/favor-plan-add", "post", favorPlanAdd) 19 | RegisterRouter("/favor-plan-remove", "post", favorPlanRemove) 20 | RegisterRouter("/favor-plan-get-list", "post", favorPlanGetList) 21 | } 22 | 23 | // check share existence 24 | func favorConfigAdd(c *gin.Context) { 25 | var req dto.FavorConfigAddReq 26 | if bindOrAbort(c, &req) != nil { 27 | return 28 | } 29 | 30 | var userID int64 31 | if getUserIDOrAbort(c, &userID) != nil { 32 | return 33 | } 34 | 35 | if configShareExistOrAbort(c, req.ID) != nil { 36 | return 37 | } 38 | 39 | // check if the share is already in user's favor 40 | var cnt int64 41 | row := db.DB.QueryRow("select count(*) from t_user_favourite_config where c_config_share_id = ? and c_user_id = ?;", 42 | req.ID, userID) 43 | if err := row.Scan(&cnt); err != nil { 44 | logrus.Error(err) 45 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 46 | return 47 | } 48 | if cnt > 0 { 49 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("this config share is already in your favor")) 50 | return 51 | } 52 | 53 | _, err := db.DB.Exec("insert into t_user_favourite_config (c_user_id, c_config_share_id) values (?, ?);", 54 | userID, req.ID) 55 | if err != nil { 56 | logrus.Error(err) 57 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 58 | return 59 | } 60 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.FavorConfigAddRes("ok"))) 61 | } 62 | 63 | func favorConfigRemove(c *gin.Context) { 64 | var req dto.FavorConfigRemoveReq 65 | if bindOrAbort(c, &req) != nil { 66 | return 67 | } 68 | 69 | var userID int64 70 | if getUserIDOrAbort(c, &userID) != nil { 71 | return 72 | } 73 | 74 | _, err := db.DB.Exec("delete from t_user_favourite_config where c_user_id = ? and c_config_share_id = ?;", 75 | userID, req.ID) 76 | if err != nil { 77 | logrus.Error(err) 78 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 79 | return 80 | } 81 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.FavorConfigRemoveRes("ok"))) 82 | } 83 | 84 | func favorConfigGetList(c *gin.Context) { 85 | var req dto.FavorConfigGetListReq 86 | if bindOrAbort(c, &req) != nil { 87 | return 88 | } 89 | 90 | var userID int64 91 | if getUserIDOrAbort(c, &userID) != nil { 92 | return 93 | } 94 | 95 | const sqlCommand string = ` 96 | select 97 | tcs.c_id, tufc.c_create_time, tc.c_name, tc.c_remark, tc.c_type, tc.c_format, tc.c_create_time, tc.c_modify_time 98 | from t_config as tc 99 | join t_config_share as tcs on tc.c_id = tcs.c_config_id 100 | join t_user_favourite_config as tufc on tcs.c_id = tufc.c_config_share_id 101 | where tc.c_deleted = false 102 | and tcs.c_deleted = false 103 | and tufc.c_user_id = ? 104 | limit ?, ?` 105 | 106 | rows, err := db.DB.Query(sqlCommand, userID, req.Offset, req.Count) 107 | defer rows.Close() 108 | if err != nil { 109 | logrus.Error(err) 110 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 111 | return 112 | } 113 | 114 | var configs []dto.FavorConfigSummary = make([]dto.FavorConfigSummary, 0) 115 | for rows.Next() { 116 | var s dto.FavorConfigSummary 117 | err = rows.Scan(&s.ShareID, &s.FavorTime, &s.Name, &s.Remark, &s.Type, &s.Format, &s.CreateTime, &s.ModifyTime) 118 | if err != nil { 119 | logrus.Error(err) 120 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 121 | return 122 | } 123 | configs = append(configs, s) 124 | } 125 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.FavorConfigGetListRes{Configs: configs})) 126 | } 127 | 128 | func favorPlanAdd(c *gin.Context) { 129 | var req dto.FavorPlanAddReq 130 | if bindOrAbort(c, &req) != nil { 131 | return 132 | } 133 | 134 | var userID int64 135 | if getUserIDOrAbort(c, &userID) != nil { 136 | return 137 | } 138 | 139 | if planShareExistOrAbort(c, req.ID) != nil { 140 | return 141 | } 142 | 143 | // check if the share is already in user's favor 144 | var cnt int64 145 | row := db.DB.QueryRow("select count(*) from t_user_favourite_plan where c_plan_share_id = ? and c_user_id = ?;", 146 | req.ID, userID) 147 | if err := row.Scan(&cnt); err != nil { 148 | logrus.Error(err) 149 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 150 | return 151 | } 152 | if cnt > 0 { 153 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("this plan share is already in your favor")) 154 | return 155 | } 156 | 157 | _, err := db.DB.Exec("insert into t_user_favourite_plan (c_user_id, c_plan_share_id) values (?, ?);", 158 | userID, req.ID) 159 | if err != nil { 160 | logrus.Error(err) 161 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 162 | return 163 | } 164 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.FavorPlanAddRes("ok"))) 165 | } 166 | 167 | func favorPlanRemove(c *gin.Context) { 168 | var req dto.FavorPlanRemoveReq 169 | if bindOrAbort(c, &req) != nil { 170 | return 171 | } 172 | 173 | var userID int64 174 | if getUserIDOrAbort(c, &userID) != nil { 175 | return 176 | } 177 | 178 | _, err := db.DB.Exec("delete from t_user_favourite_plan where c_user_id = ? and c_plan_share_id = ?;", 179 | userID, req.ID) 180 | if err != nil { 181 | logrus.Error(err) 182 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 183 | return 184 | } 185 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.FavorPlanRemoveRes("ok"))) 186 | } 187 | 188 | func favorPlanGetList(c *gin.Context) { 189 | var req dto.FavorPlanGetListReq 190 | if bindOrAbort(c, &req) != nil { 191 | return 192 | } 193 | 194 | var userID int64 195 | if getUserIDOrAbort(c, &userID) != nil { 196 | return 197 | } 198 | 199 | const sqlCommand = ` 200 | select tps.c_id, tufp.c_create_time, tp.c_name, tp.c_remark, tp.c_create_time, tp.c_modify_time 201 | from t_plan as tp 202 | join t_plan_share as tps on tp.c_id = tps.c_plan_id 203 | join t_user_favourite_plan as tufp on tps.c_id = tufp.c_plan_share_id 204 | where tp.c_deleted = false 205 | and tps.c_deleted = false 206 | and tufp.c_user_id = ? 207 | limit ?, ?;` 208 | rows, err := db.DB.Query(sqlCommand, userID, req.Offset, req.Count) 209 | if err != nil { 210 | logrus.Error(err) 211 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 212 | return 213 | } 214 | 215 | var plans []dto.FavorPlanSummary = make([]dto.FavorPlanSummary, 0) 216 | for rows.Next() { 217 | var t dto.FavorPlanSummary 218 | err = rows.Scan(&t.ShareID, &t.FavorTime, &t.Name, &t.Remark, &t.CreateTime, &t.ModifyTime) 219 | if err != nil { 220 | logrus.Error(err) 221 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err)) 222 | return 223 | } 224 | plans = append(plans, t) 225 | } 226 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.FavorPlanGetListRes{Plans: plans})) 227 | } 228 | -------------------------------------------------------------------------------- /routers/share.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/leafee98/class-schedule-to-icalendar-restserver/db" 10 | "github.com/leafee98/class-schedule-to-icalendar-restserver/dto" 11 | "github.com/leafee98/class-schedule-to-icalendar-restserver/middlewares" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func getUserIDOrAbort(c *gin.Context, userID *int64) error { 16 | idInterface, exist := c.Get(middlewares.Key.UserID) 17 | if exist == false { 18 | c.AbortWithStatusJSON(http.StatusForbidden, 19 | dto.NewResponseBad("unauthorized action is forbidden")) 20 | return errors.New("unanthorized") 21 | } 22 | *userID = idInterface.(int64) 23 | return nil 24 | } 25 | 26 | func checkConfigTypeRange(t int8) bool { 27 | return t <= dto.LimitConfigTypeMax && t >= dto.LimitConfigFormatMin 28 | } 29 | 30 | func checkConfigFormatRange(r int8) bool { 31 | return r <= dto.LimitConfigFormatMax && r >= dto.LimitConfigFormatMin 32 | } 33 | 34 | func bindOrAbort(c *gin.Context, req interface{}) error { 35 | err := c.ShouldBind(req) 36 | if err != nil { 37 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("invalid request parameters")) 38 | } 39 | return err 40 | } 41 | 42 | /////////////////////////////// 43 | /////// Database Utility ////// 44 | /////////////////////////////// 45 | 46 | ///////// Plan Part /////////// 47 | 48 | // return nil if plan exist. 49 | func planExist(planID int64) error { 50 | row := db.DB.QueryRow("select c_id from t_plan where c_id = ? and c_deleted = false", planID) 51 | return row.Scan(&planID) 52 | } 53 | 54 | func planExistOrAbort(c *gin.Context, planID int64) error { 55 | err := planExist(planID) 56 | if err != nil { 57 | if err == sql.ErrNoRows { 58 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("the plan not exist")) 59 | } else { 60 | logrus.Error(err.Error()) 61 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 62 | } 63 | } 64 | return err 65 | } 66 | 67 | // return nil if the plan belongs to the user 68 | func planOwnerShip(planID int64, userID int64) error { 69 | var userIDInDB int64 70 | row := db.DB.QueryRow("select c_owner_id from t_plan where c_id = ? and c_deleted = false", planID) 71 | err := row.Scan(&userIDInDB) 72 | if err == sql.ErrNoRows { 73 | return errors.New("the plan not exist") 74 | } else if err != nil { 75 | return err 76 | } 77 | if userIDInDB != userID { 78 | return errors.New("you are not the owner of the plan") 79 | } 80 | return nil 81 | } 82 | 83 | func planOwnershipOrAbort(c *gin.Context, planID int64, userID int64) error { 84 | err := planOwnerShip(planID, userID) 85 | if err != nil { 86 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad(err.Error())) 87 | } 88 | return err 89 | } 90 | 91 | //////// Config Part ////////// 92 | 93 | // return nil if the config exist 94 | func configExist(configID int64) error { 95 | row := db.DB.QueryRow("select c_id from t_plan where c_id = ? and c_deleted = false", configID) 96 | return row.Scan(&configID) 97 | } 98 | 99 | func configExistOrAbort(c *gin.Context, configID int64) error { 100 | err := configExist(configID) 101 | if err != nil { 102 | if err == sql.ErrNoRows { 103 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("the config not exist")) 104 | } else { 105 | logrus.Error(err.Error()) 106 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 107 | } 108 | } 109 | return err 110 | } 111 | 112 | // return nil if the config belongs to the user 113 | func configOwnership(configID int64, userID int64) error { 114 | var userIDInDB int64 115 | row := db.DB.QueryRow("select c_owner_id from t_config where c_id = ? and c_deleted = false", configID) 116 | err := row.Scan(&userIDInDB) 117 | if err == sql.ErrNoRows { 118 | return errors.New("the config not exist") 119 | } else if err != nil { 120 | return err 121 | } 122 | if userIDInDB != userID { 123 | return errors.New("you are not the owner of the plan") 124 | } 125 | return nil 126 | } 127 | 128 | func configOwnershipOrAbort(c *gin.Context, configID int64, userID int64) error { 129 | err := configOwnership(configID, userID) 130 | if err != nil { 131 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad(err.Error())) 132 | } 133 | return err 134 | } 135 | 136 | /////// Relation Part ///////// 137 | 138 | // return nil if exist 139 | func relationExist(planID int64, configID int64) error { 140 | var cnt int64 141 | row := db.DB.QueryRow("select count(*) as cnt from t_plan_config_relation"+ 142 | " where c_plan_id = ? and c_config_id = ?", planID, configID) 143 | err := row.Scan(&cnt) 144 | 145 | // sql.ErrNoRows will never occur 146 | if err != nil { 147 | return err 148 | } else { 149 | if cnt > 0 { 150 | return nil 151 | } else { 152 | return errors.New("no such relation") 153 | } 154 | } 155 | } 156 | 157 | ///// Relation share Part ///// 158 | 159 | // return nil if exist 160 | func relationShareExist(planID int64, configShareID int64) error { 161 | var cnt int64 162 | row := db.DB.QueryRow("select count(*) as cnt from t_plan_config_share_relation "+ 163 | "where c_plan_id = ? and c_config_share_id = ?", planID, configShareID) 164 | err := row.Scan(&cnt) 165 | 166 | // sql.ErrNoRows will never occur 167 | if err != nil { 168 | return err 169 | } else { 170 | if cnt > 0 { 171 | return nil 172 | } else { 173 | return errors.New("no such relation") 174 | } 175 | } 176 | } 177 | 178 | ////// Config Share Part ////// 179 | 180 | func configShareExist(configShareID int64) error { 181 | var count int64 182 | row := db.DB.QueryRow("select count(*) from t_config_share where c_deleted = false and c_id = ?;", configShareID) 183 | if err := row.Scan(&count); err != nil { 184 | return err 185 | } else { 186 | if count > 0 { 187 | return nil 188 | } else { 189 | return errors.New("config share not exist or has been deleted") 190 | } 191 | } 192 | } 193 | 194 | func configShareExistOrAbort(c *gin.Context, configShareID int64) error { 195 | err := configShareExist(configShareID) 196 | if err != nil { 197 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad(err.Error())) 198 | } 199 | return err 200 | } 201 | 202 | func configShareOwnership(configShareID int64, userID int64) error { 203 | var dbUserID int64 204 | const sqlCommand string = "select c_owner_id from t_config where c_deleted = false and c_id = " + 205 | "(select c_config_id from t_config_share where c_deleted = false and c_id = ?);" 206 | row := db.DB.QueryRow(sqlCommand, configShareID) 207 | err := row.Scan(&dbUserID) 208 | if err == sql.ErrNoRows { 209 | return errors.New("the config share doesn't exist") 210 | } else if err != nil { 211 | return err 212 | } 213 | if dbUserID != userID { 214 | return errors.New("you are not the owner of the config's share") 215 | } 216 | return nil 217 | } 218 | 219 | func configShareOwnershipOrAbort(c *gin.Context, configShareID int64, userID int64) error { 220 | err := configShareOwnership(configShareID, userID) 221 | if err != nil { 222 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad(err.Error())) 223 | } 224 | return err 225 | } 226 | 227 | /////// Plan Share Part /////// 228 | 229 | func planShareExist(configShareID int64) error { 230 | var count int64 231 | row := db.DB.QueryRow("select count(*) from t_plan_share where c_deleted = false and c_id = ?;", configShareID) 232 | if err := row.Scan(&count); err != nil { 233 | return err 234 | } else { 235 | if count > 0 { 236 | return nil 237 | } else { 238 | return errors.New("plan share not exist or has been deleted") 239 | } 240 | } 241 | } 242 | 243 | func planShareExistOrAbort(c *gin.Context, configShareID int64) error { 244 | err := planShareExist(configShareID) 245 | if err != nil { 246 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad(err.Error())) 247 | } 248 | return err 249 | } 250 | 251 | func planShareOwnership(configShareID int64, userID int64) error { 252 | var dbUserID int64 253 | const sqlCommand string = "select c_owner_id from t_plan where c_deleted = false and c_id = " + 254 | "(select c_plan_id from t_plan_share where c_deleted = false and c_id = ?);" 255 | row := db.DB.QueryRow(sqlCommand, configShareID) 256 | err := row.Scan(&dbUserID) 257 | if err == sql.ErrNoRows { 258 | return errors.New("the config share doesn't exist") 259 | } else if err != nil { 260 | return err 261 | } 262 | if dbUserID != userID { 263 | return errors.New("you are not the owner of the config's share") 264 | } 265 | return nil 266 | } 267 | 268 | func planShareOwnershipOrAbort(c *gin.Context, configShareID int64, userID int64) error { 269 | err := planShareOwnership(configShareID, userID) 270 | if err != nil { 271 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad(err.Error())) 272 | } 273 | return err 274 | } 275 | -------------------------------------------------------------------------------- /api-doc.txt: -------------------------------------------------------------------------------- 1 | ================================================== 2 | =================== user part ==================== 3 | ================================================== 4 | 5 | -------------------------------------------------- 6 | /register 7 | 8 | post: 9 | username: string 10 | password: string 11 | nickname: string 12 | email: string 13 | response data: 14 | id: int // id of this user 15 | 16 | -------------------------------------------------- 17 | /login 18 | 19 | post: 20 | username: string 21 | password: string 22 | tokenDuration: int // token's valid period (in days) 23 | response data: 24 | id: int // id of this user 25 | 26 | -------------------------------------------------- 27 | /logout 28 | 29 | post: 30 | // no parameter 31 | get: 32 | // no parameter 33 | response data: 34 | // no parameter 35 | 36 | ================================================== 37 | ================= config part ==================== 38 | ================================================== 39 | 40 | -------------------------------------------------- 41 | /config-create 42 | 43 | post: 44 | name: string 45 | type: int // 1: global, 2: lesson 46 | format: int // 1: json, 2:toml (not supported) 47 | content: string 48 | remark: string 49 | response: 50 | id: int 51 | 52 | -------------------------------------------------- 53 | /config-get-by-id 54 | 55 | post: 56 | id: int 57 | response data: 58 | id: int 59 | type: int 60 | format: int 61 | name: string 62 | content: string 63 | remark: string 64 | createTime: string time // YYYY-MM-DDThh:mm:ssZ 65 | modifyTime: string time // YYYY-MM-DDThh:mm:ssZ 66 | 67 | -------------------------------------------------- 68 | /config-get-by-share 69 | 70 | post: 71 | id: int 72 | response data: 73 | id: int 74 | type: int 75 | format: int 76 | name: string 77 | content: string 78 | remark: string 79 | createTime: string time // YYYY-MM-DDThh:mm:ssZ 80 | modifyTime: string time // YYYY-MM-DDThh:mm:ssZ 81 | 82 | -------------------------------------------------- 83 | /config-modify 84 | 85 | post: 86 | id: int 87 | name: string 88 | content: string 89 | format: int 90 | remark: string 91 | response data: 92 | // no response data 93 | 94 | -------------------------------------------------- 95 | /config-remove 96 | 97 | post: 98 | id: int 99 | response data: 100 | // no response data 101 | 102 | -------------------------------------------------- 103 | /config-get-list 104 | 105 | post: 106 | sortBy: string // 'id', 'name', 'createTime', 'modifyTime' 107 | offset: int 108 | count: int // mo more than 30 109 | response: 110 | configs: ConfigSummary[] 111 | 112 | ConfigSummary: 113 | id: int 114 | type: int 115 | name: string 116 | format: int 117 | remark: string 118 | createTime: string time 119 | modifyTime: string time 120 | 121 | -------------------------------------------------- 122 | /config-share-create 123 | 124 | post: 125 | id: int 126 | remark: string 127 | response: 128 | id: int // created share's id 129 | 130 | -------------------------------------------------- 131 | /config-share-modify 132 | 133 | post: 134 | id: int // share id 135 | remark: string 136 | response data: 137 | // no response data 138 | 139 | -------------------------------------------------- 140 | /config-share-revoke 141 | 142 | post: 143 | id: int // share id 144 | response data: 145 | // no response data 146 | 147 | -------------------------------------------------- 148 | /config-share-get-list 149 | 150 | post: 151 | id: int // config id 152 | response data: 153 | shares: ConfigShareDetail[] 154 | 155 | ConfigShareDetail: 156 | id: int // share id 157 | remark: string 158 | createTime: string time 159 | 160 | 161 | ================================================== 162 | ================== plan part ===================== 163 | ================================================== 164 | 165 | -------------------------------------------------- 166 | /plan-create 167 | 168 | post: 169 | name string 170 | remark string 171 | response data: 172 | id: int 173 | 174 | -------------------------------------------------- 175 | /plan-add-config 176 | 177 | post: 178 | planId: int 179 | configId: int 180 | response: 181 | // no response data 182 | 183 | -------------------------------------------------- 184 | /plan-remove-config 185 | 186 | post: 187 | planId: int 188 | configId: int 189 | response: 190 | // no response data 191 | 192 | -------------------------------------------------- 193 | /plan-add-share 194 | 195 | post: 196 | planId: int 197 | configShareId: int 198 | response: 199 | // no response data 200 | 201 | -------------------------------------------------- 202 | /plan-remove-share 203 | 204 | post: 205 | planId: int 206 | configShareId: int 207 | response: 208 | // no response data 209 | 210 | -------------------------------------------------- 211 | /plan-get-by-id 212 | 213 | post: 214 | id: int 215 | response: 216 | name: string 217 | remark: string 218 | id: int 219 | createTime: string time 220 | modifyTime: string time 221 | configs: ConfigDetail 222 | shares: ConfigDetail // share id replace config id 223 | 224 | ConfigDetail: 225 | id: int 226 | type: int 227 | format: int 228 | name: string 229 | content: string 230 | remark: string 231 | createTime: string time 232 | modifyTime: string time 233 | 234 | -------------------------------------------------- 235 | /plan-get-by-share 236 | 237 | post: 238 | id: int // share id 239 | response: 240 | ... 241 | // the same as `/plan-get-by-share` 242 | 243 | -------------------------------------------------- 244 | /plan-remove 245 | 246 | post: 247 | id: int 248 | response: 249 | // no response data 250 | 251 | -------------------------------------------------- 252 | /plan-modify 253 | 254 | post: 255 | id: int 256 | name: string 257 | remark: string 258 | response: 259 | // no response data 260 | 261 | -------------------------------------------------- 262 | /plan-get-list 263 | 264 | post: 265 | sortby: string // 'id', 'name', 'createTime', 'modifyTime' 266 | offset: int 267 | count: int // no more than 30 268 | response: 269 | plans: PlanSummary[] 270 | 271 | PlanSummary: 272 | id: int 273 | name: string 274 | remark string 275 | createTime: string time 276 | modifyTime: string time 277 | 278 | -------------------------------------------------- 279 | /plan-create-token 280 | 281 | post: 282 | id: int // id of plan 283 | response: 284 | token: string 285 | 286 | -------------------------------------------------- 287 | /plan-revoke-token 288 | 289 | post: 290 | token: string // token created 291 | response: 292 | // no response data 293 | 294 | -------------------------------------------------- 295 | /plan-get-token-list 296 | 297 | post: 298 | id: int // id of plan 299 | response: 300 | tokens: PlanTokenDetail[] 301 | 302 | PlanTokenDetail: 303 | token: string 304 | createTime: string time 305 | 306 | -------------------------------------------------- 307 | /plan-share-create 308 | 309 | post: 310 | id: int // id of plan 311 | remark: string 312 | response: 313 | id: int // id of share 314 | 315 | -------------------------------------------------- 316 | /plan-share-modify 317 | 318 | post: 319 | id: int // id of share 320 | remark: string 321 | response: 322 | // no response data 323 | 324 | -------------------------------------------------- 325 | /plan-share-revoke 326 | 327 | post: 328 | id: int // share id 329 | response: 330 | // no response data 331 | 332 | -------------------------------------------------- 333 | /plan-share-get-list 334 | 335 | post: 336 | id: int // id of plan 337 | response: 338 | shares: PlanShareDetail[] 339 | 340 | PlanShareDetail: 341 | id: int // share id 342 | remark: string 343 | createTime: string time 344 | 345 | 346 | ================================================== 347 | ================= favor part ===================== 348 | ================================================== 349 | 350 | -------------------------------------------------- 351 | /favor-config-add 352 | 353 | post: 354 | id: int // id of config share 355 | response: 356 | // no response data 357 | 358 | 359 | -------------------------------------------------- 360 | /favor-config-remove 361 | 362 | post: 363 | id: int // id of config share 364 | response: 365 | // no response data 366 | 367 | 368 | -------------------------------------------------- 369 | /favor-config-get-list 370 | 371 | post: 372 | offset: int 373 | count: int 374 | response: 375 | configs: FavorConfigSummary[] 376 | 377 | FavorConfigSummary: 378 | shareId: int 379 | type: int 380 | format: int 381 | name: string 382 | remark: string 383 | favorTime: string time 384 | createTime: string time 385 | modifyTime: string time 386 | 387 | 388 | -------------------------------------------------- 389 | /favor-plan-add 390 | 391 | post: 392 | id: int // id of plan share 393 | response: 394 | // no response data 395 | 396 | 397 | -------------------------------------------------- 398 | /favor-plan-remove 399 | 400 | post: 401 | id: int // id of plan share 402 | response: 403 | // no response data 404 | 405 | 406 | -------------------------------------------------- 407 | /favor-plan-get-list 408 | 409 | post: 410 | offset: int 411 | count: int 412 | response: 413 | plans: FavorPlanSummary[] 414 | 415 | FavorPlanSummary: 416 | shareId: int 417 | name: string 418 | remark: string 419 | favorTime: string time 420 | createTime: string time 421 | modifyTime: string time 422 | 423 | 424 | ================================================== 425 | ================= generate part ================== 426 | ================================================== 427 | 428 | -------------------------------------------------- 429 | /generate-by-plan-token 430 | 431 | get: 432 | token: string 433 | response: 434 | // plain text, generate result 435 | -------------------------------------------------------------------------------- /rpc/CSTIRPC/CSTIRpcServer.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.25.0 4 | // protoc v3.12.4 5 | // source: CSTIRpcServer.proto 6 | 7 | package CSTIRPC 8 | 9 | import ( 10 | proto "github.com/golang/protobuf/proto" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // This is a compile-time assertion that a sufficiently up-to-date version 25 | // of the legacy proto package is being used. 26 | const _ = proto.ProtoPackageIsVersion4 27 | 28 | type ConfJson struct { 29 | state protoimpl.MessageState 30 | sizeCache protoimpl.SizeCache 31 | unknownFields protoimpl.UnknownFields 32 | 33 | Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` 34 | } 35 | 36 | func (x *ConfJson) Reset() { 37 | *x = ConfJson{} 38 | if protoimpl.UnsafeEnabled { 39 | mi := &file_CSTIRpcServer_proto_msgTypes[0] 40 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 41 | ms.StoreMessageInfo(mi) 42 | } 43 | } 44 | 45 | func (x *ConfJson) String() string { 46 | return protoimpl.X.MessageStringOf(x) 47 | } 48 | 49 | func (*ConfJson) ProtoMessage() {} 50 | 51 | func (x *ConfJson) ProtoReflect() protoreflect.Message { 52 | mi := &file_CSTIRpcServer_proto_msgTypes[0] 53 | if protoimpl.UnsafeEnabled && x != nil { 54 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 55 | if ms.LoadMessageInfo() == nil { 56 | ms.StoreMessageInfo(mi) 57 | } 58 | return ms 59 | } 60 | return mi.MessageOf(x) 61 | } 62 | 63 | // Deprecated: Use ConfJson.ProtoReflect.Descriptor instead. 64 | func (*ConfJson) Descriptor() ([]byte, []int) { 65 | return file_CSTIRpcServer_proto_rawDescGZIP(), []int{0} 66 | } 67 | 68 | func (x *ConfJson) GetContent() string { 69 | if x != nil { 70 | return x.Content 71 | } 72 | return "" 73 | } 74 | 75 | type ConfToml struct { 76 | state protoimpl.MessageState 77 | sizeCache protoimpl.SizeCache 78 | unknownFields protoimpl.UnknownFields 79 | 80 | Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` 81 | } 82 | 83 | func (x *ConfToml) Reset() { 84 | *x = ConfToml{} 85 | if protoimpl.UnsafeEnabled { 86 | mi := &file_CSTIRpcServer_proto_msgTypes[1] 87 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 88 | ms.StoreMessageInfo(mi) 89 | } 90 | } 91 | 92 | func (x *ConfToml) String() string { 93 | return protoimpl.X.MessageStringOf(x) 94 | } 95 | 96 | func (*ConfToml) ProtoMessage() {} 97 | 98 | func (x *ConfToml) ProtoReflect() protoreflect.Message { 99 | mi := &file_CSTIRpcServer_proto_msgTypes[1] 100 | if protoimpl.UnsafeEnabled && x != nil { 101 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 102 | if ms.LoadMessageInfo() == nil { 103 | ms.StoreMessageInfo(mi) 104 | } 105 | return ms 106 | } 107 | return mi.MessageOf(x) 108 | } 109 | 110 | // Deprecated: Use ConfToml.ProtoReflect.Descriptor instead. 111 | func (*ConfToml) Descriptor() ([]byte, []int) { 112 | return file_CSTIRpcServer_proto_rawDescGZIP(), []int{1} 113 | } 114 | 115 | func (x *ConfToml) GetContent() string { 116 | if x != nil { 117 | return x.Content 118 | } 119 | return "" 120 | } 121 | 122 | type ResultIcal struct { 123 | state protoimpl.MessageState 124 | sizeCache protoimpl.SizeCache 125 | unknownFields protoimpl.UnknownFields 126 | 127 | Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` 128 | } 129 | 130 | func (x *ResultIcal) Reset() { 131 | *x = ResultIcal{} 132 | if protoimpl.UnsafeEnabled { 133 | mi := &file_CSTIRpcServer_proto_msgTypes[2] 134 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 135 | ms.StoreMessageInfo(mi) 136 | } 137 | } 138 | 139 | func (x *ResultIcal) String() string { 140 | return protoimpl.X.MessageStringOf(x) 141 | } 142 | 143 | func (*ResultIcal) ProtoMessage() {} 144 | 145 | func (x *ResultIcal) ProtoReflect() protoreflect.Message { 146 | mi := &file_CSTIRpcServer_proto_msgTypes[2] 147 | if protoimpl.UnsafeEnabled && x != nil { 148 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 149 | if ms.LoadMessageInfo() == nil { 150 | ms.StoreMessageInfo(mi) 151 | } 152 | return ms 153 | } 154 | return mi.MessageOf(x) 155 | } 156 | 157 | // Deprecated: Use ResultIcal.ProtoReflect.Descriptor instead. 158 | func (*ResultIcal) Descriptor() ([]byte, []int) { 159 | return file_CSTIRpcServer_proto_rawDescGZIP(), []int{2} 160 | } 161 | 162 | func (x *ResultIcal) GetContent() string { 163 | if x != nil { 164 | return x.Content 165 | } 166 | return "" 167 | } 168 | 169 | var File_CSTIRpcServer_proto protoreflect.FileDescriptor 170 | 171 | var file_CSTIRpcServer_proto_rawDesc = []byte{ 172 | 0x0a, 0x13, 0x43, 0x53, 0x54, 0x49, 0x52, 0x70, 0x63, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 173 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x72, 0x70, 0x63, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 174 | 0x22, 0x24, 0x0a, 0x08, 0x43, 0x6f, 0x6e, 0x66, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 175 | 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 176 | 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x24, 0x0a, 0x08, 0x43, 0x6f, 0x6e, 0x66, 0x54, 0x6f, 177 | 0x6d, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 178 | 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x26, 0x0a, 0x0a, 179 | 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x49, 0x63, 0x61, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 180 | 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 181 | 0x74, 0x65, 0x6e, 0x74, 0x32, 0x8b, 0x01, 0x0a, 0x0d, 0x43, 0x53, 0x54, 0x49, 0x52, 0x70, 0x63, 182 | 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x3c, 0x0a, 0x0c, 0x6a, 0x73, 0x6f, 0x6e, 0x47, 0x65, 183 | 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x12, 0x13, 0x2e, 0x72, 0x70, 0x63, 0x73, 0x65, 0x72, 0x76, 184 | 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x4a, 0x73, 0x6f, 0x6e, 0x1a, 0x15, 0x2e, 0x72, 0x70, 185 | 0x63, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x49, 0x63, 186 | 0x61, 0x6c, 0x22, 0x00, 0x12, 0x3c, 0x0a, 0x0c, 0x74, 0x6f, 0x6d, 0x6c, 0x47, 0x65, 0x6e, 0x65, 187 | 0x72, 0x61, 0x74, 0x65, 0x12, 0x13, 0x2e, 0x72, 0x70, 0x63, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 188 | 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x54, 0x6f, 0x6d, 0x6c, 0x1a, 0x15, 0x2e, 0x72, 0x70, 0x63, 0x73, 189 | 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x49, 0x63, 0x61, 0x6c, 190 | 0x22, 0x00, 0x42, 0x86, 0x01, 0x0a, 0x26, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 191 | 0x62, 0x2e, 0x6c, 0x65, 0x61, 0x66, 0x65, 0x65, 0x39, 0x38, 0x2e, 0x43, 0x53, 0x54, 0x49, 0x2e, 192 | 0x72, 0x70, 0x63, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x72, 0x70, 0x63, 0x42, 0x12, 0x43, 193 | 0x53, 0x54, 0x49, 0x52, 0x70, 0x63, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 194 | 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 195 | 0x6c, 0x65, 0x61, 0x66, 0x65, 0x65, 0x39, 0x38, 0x2f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x2d, 0x73, 196 | 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x2d, 0x74, 0x6f, 0x2d, 0x69, 0x63, 0x61, 0x6c, 0x65, 197 | 0x6e, 0x64, 0x61, 0x72, 0x2d, 0x72, 0x65, 0x73, 0x74, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 198 | 0x72, 0x70, 0x63, 0x2f, 0x43, 0x53, 0x54, 0x49, 0x52, 0x50, 0x43, 0x62, 0x06, 0x70, 0x72, 0x6f, 199 | 0x74, 0x6f, 0x33, 200 | } 201 | 202 | var ( 203 | file_CSTIRpcServer_proto_rawDescOnce sync.Once 204 | file_CSTIRpcServer_proto_rawDescData = file_CSTIRpcServer_proto_rawDesc 205 | ) 206 | 207 | func file_CSTIRpcServer_proto_rawDescGZIP() []byte { 208 | file_CSTIRpcServer_proto_rawDescOnce.Do(func() { 209 | file_CSTIRpcServer_proto_rawDescData = protoimpl.X.CompressGZIP(file_CSTIRpcServer_proto_rawDescData) 210 | }) 211 | return file_CSTIRpcServer_proto_rawDescData 212 | } 213 | 214 | var file_CSTIRpcServer_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 215 | var file_CSTIRpcServer_proto_goTypes = []interface{}{ 216 | (*ConfJson)(nil), // 0: rpcserver.ConfJson 217 | (*ConfToml)(nil), // 1: rpcserver.ConfToml 218 | (*ResultIcal)(nil), // 2: rpcserver.ResultIcal 219 | } 220 | var file_CSTIRpcServer_proto_depIdxs = []int32{ 221 | 0, // 0: rpcserver.CSTIRpcServer.jsonGenerate:input_type -> rpcserver.ConfJson 222 | 1, // 1: rpcserver.CSTIRpcServer.tomlGenerate:input_type -> rpcserver.ConfToml 223 | 2, // 2: rpcserver.CSTIRpcServer.jsonGenerate:output_type -> rpcserver.ResultIcal 224 | 2, // 3: rpcserver.CSTIRpcServer.tomlGenerate:output_type -> rpcserver.ResultIcal 225 | 2, // [2:4] is the sub-list for method output_type 226 | 0, // [0:2] is the sub-list for method input_type 227 | 0, // [0:0] is the sub-list for extension type_name 228 | 0, // [0:0] is the sub-list for extension extendee 229 | 0, // [0:0] is the sub-list for field type_name 230 | } 231 | 232 | func init() { file_CSTIRpcServer_proto_init() } 233 | func file_CSTIRpcServer_proto_init() { 234 | if File_CSTIRpcServer_proto != nil { 235 | return 236 | } 237 | if !protoimpl.UnsafeEnabled { 238 | file_CSTIRpcServer_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 239 | switch v := v.(*ConfJson); i { 240 | case 0: 241 | return &v.state 242 | case 1: 243 | return &v.sizeCache 244 | case 2: 245 | return &v.unknownFields 246 | default: 247 | return nil 248 | } 249 | } 250 | file_CSTIRpcServer_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 251 | switch v := v.(*ConfToml); i { 252 | case 0: 253 | return &v.state 254 | case 1: 255 | return &v.sizeCache 256 | case 2: 257 | return &v.unknownFields 258 | default: 259 | return nil 260 | } 261 | } 262 | file_CSTIRpcServer_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 263 | switch v := v.(*ResultIcal); i { 264 | case 0: 265 | return &v.state 266 | case 1: 267 | return &v.sizeCache 268 | case 2: 269 | return &v.unknownFields 270 | default: 271 | return nil 272 | } 273 | } 274 | } 275 | type x struct{} 276 | out := protoimpl.TypeBuilder{ 277 | File: protoimpl.DescBuilder{ 278 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 279 | RawDescriptor: file_CSTIRpcServer_proto_rawDesc, 280 | NumEnums: 0, 281 | NumMessages: 3, 282 | NumExtensions: 0, 283 | NumServices: 1, 284 | }, 285 | GoTypes: file_CSTIRpcServer_proto_goTypes, 286 | DependencyIndexes: file_CSTIRpcServer_proto_depIdxs, 287 | MessageInfos: file_CSTIRpcServer_proto_msgTypes, 288 | }.Build() 289 | File_CSTIRpcServer_proto = out.File 290 | file_CSTIRpcServer_proto_rawDesc = nil 291 | file_CSTIRpcServer_proto_goTypes = nil 292 | file_CSTIRpcServer_proto_depIdxs = nil 293 | } 294 | -------------------------------------------------------------------------------- /routers/config.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/leafee98/class-schedule-to-icalendar-restserver/db" 10 | "github.com/leafee98/class-schedule-to-icalendar-restserver/dto" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func init() { 15 | RegisterRouter("/config-create", "post", configCreate) 16 | RegisterRouter("/config-get-by-id", "post", configGetByID) 17 | RegisterRouter("/config-get-by-share", "post", configGetByShare) 18 | RegisterRouter("/config-modify", "post", configModify) 19 | RegisterRouter("/config-remove", "post", configRemove) 20 | RegisterRouter("/config-get-list", "post", configGetList) 21 | 22 | RegisterRouter("/config-share-create", "post", configShareCreate) 23 | RegisterRouter("/config-share-modify", "post", configShareModify) 24 | RegisterRouter("/config-share-revoke", "post", configShareRevoke) 25 | RegisterRouter("/config-share-get-list", "post", configShareGetList) 26 | } 27 | 28 | // ConfigCreate will create a new Config 29 | // 30 | // Check if the user is authorized 31 | // Check the type and format is in range of rule, err: "invalid type or format" 32 | func configCreate(c *gin.Context) { 33 | var req dto.ConfigCreateReq 34 | if bindOrAbort(c, &req) != nil { 35 | return 36 | } 37 | 38 | // if the user is authorzied 39 | var ownerID int64 40 | if getUserIDOrAbort(c, &ownerID) != nil { 41 | return 42 | } 43 | 44 | // check config's type and format in range of rule 45 | if !checkConfigFormatRange(req.Format) || !checkConfigTypeRange(req.Type) { 46 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("invalid type or format")) 47 | return 48 | } 49 | 50 | // insert the created config 51 | res, err := db.DB.Exec( 52 | "insert into t_config (c_type, c_name, c_content, c_format, c_owner_id, c_remark)"+ 53 | " values (?, ?, ?, ?, ?, ?)", 54 | req.Type, req.Name, req.Content, req.Format, ownerID, req.Remark) 55 | if err != nil { 56 | c.AbortWithStatusJSON(http.StatusInternalServerError, dto.NewResponseBad(err.Error())) 57 | return 58 | } 59 | 60 | configID, _ := res.LastInsertId() 61 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.ConfigCreateRes{ID: configID})) 62 | } 63 | 64 | // only owner could get config by id 65 | // 66 | // check login status, err msg: "unauthorized action is forbidden" 67 | // check the deleted status, or no rows got, err msg: "config not exists" 68 | // check the ownership, err msg: "you are not the owner of the config" 69 | func configGetByID(c *gin.Context) { 70 | // bind request 71 | var req dto.ConfigGetByIDReq 72 | if bindOrAbort(c, &req) != nil { 73 | return 74 | } 75 | 76 | // check login status 77 | var userID int64 78 | if getUserIDOrAbort(c, &userID) != nil { 79 | return 80 | } 81 | 82 | // get the config detail 83 | var res dto.ConfigGetRes 84 | var ownerID int64 85 | row := db.DB.QueryRow( 86 | "select c_id, c_type, c_name, c_content, c_format, c_remark, c_create_time, c_modify_time, c_owner_id "+ 87 | "from t_config where c_deleted = false and c_id = ?", req.ID) 88 | err := row.Scan(&res.ID, &res.Type, &res.Name, &res.Content, &res.Format, 89 | &res.Remark, &res.CreateTime, &res.ModifyTime, &ownerID) 90 | 91 | if err != nil { 92 | if err == sql.ErrNoRows { 93 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("config not exists")) 94 | } else { 95 | logrus.Error(err.Error()) 96 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 97 | } 98 | } else { 99 | // check ownership 100 | if userID == ownerID { 101 | c.JSON(http.StatusOK, dto.NewResponseFine(res)) 102 | } else { 103 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("you are not the owner of the config")) 104 | } 105 | } 106 | } 107 | 108 | // no need to login 109 | func configGetByShare(c *gin.Context) { 110 | // bind request 111 | var req dto.ConfigGetByShareReq 112 | if bindOrAbort(c, &req) != nil { 113 | return 114 | } 115 | 116 | // get the config detail from share id 117 | var res dto.ConfigGetRes 118 | var ownerID int64 119 | row := db.DB.QueryRow( 120 | "select c_id, c_type, c_name, c_content, c_format, c_remark, c_create_time, c_modify_time, c_owner_id "+ 121 | "from t_config where c_deleted = false and c_id = "+ 122 | "(select c_config_id from t_config_share where c_id = ?);", req.ID) 123 | err := row.Scan(&res.ID, &res.Type, &res.Name, &res.Content, &res.Format, 124 | &res.Remark, &res.CreateTime, &res.ModifyTime, &ownerID) 125 | 126 | if err != nil { 127 | if err == sql.ErrNoRows { 128 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("config or config share not exists")) 129 | } else { 130 | logrus.Error(err.Error()) 131 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 132 | } 133 | } else { 134 | c.JSON(http.StatusOK, dto.NewResponseFine(res)) 135 | } 136 | } 137 | 138 | // other user could get config by share's id 139 | // 140 | // check the share link exists or not, err msg: "share link not found" 141 | // check the share link expired status, err msg: "share link expired" 142 | // check the config deleted status, err msg: "config not exists" 143 | func configGetConfigByShare(c *gin.Context) { 144 | 145 | } 146 | 147 | // change config content 148 | // property Type is not changable 149 | // 150 | // check login status, err msg: "unauthorized action is forbidden" 151 | // check the deleted status, or no rows got, err msg: "config not exists" 152 | // check the ownership, err msg: "you are not the owner of the config" 153 | // update c_modify_time 154 | func configModify(c *gin.Context) { 155 | // bind request 156 | var req dto.ConfigModifyReq 157 | if bindOrAbort(c, &req) != nil { 158 | return 159 | } 160 | 161 | // check login status 162 | var userID int64 163 | if getUserIDOrAbort(c, &userID) != nil { 164 | return 165 | } 166 | 167 | // get the config detail 168 | var deleted bool 169 | var ownerID int64 170 | row := db.DB.QueryRow("select c_deleted, c_owner_id from t_config where c_id = ?", req.ID) 171 | err := row.Scan(&deleted, &ownerID) 172 | 173 | if err != sql.ErrNoRows && err != nil { 174 | // unknown error 175 | logrus.Error(err.Error()) 176 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 177 | return 178 | } else if err == sql.ErrNoRows || deleted { 179 | // no such config or deleted 180 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("config not exists")) 181 | return 182 | } else if err == nil { 183 | // check ownership 184 | if userID != ownerID { 185 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("you are not the owner of the config")) 186 | return 187 | } 188 | } 189 | 190 | // update the config 191 | _, err = db.DB.Exec("update t_config"+ 192 | " set c_name=?, c_content=?, c_format=?, c_remark=?"+ 193 | " where c_id = ?", 194 | req.Name, req.Content, req.Format, req.Remark, req.ID) 195 | if err != nil { 196 | logrus.Error(err.Error()) 197 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 198 | } else { 199 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.ConfigModifyRes("ok"))) 200 | } 201 | } 202 | 203 | // remove the config (set the deleted flag to true) 204 | // check login 205 | // check exists 206 | // check ownership 207 | func configRemove(c *gin.Context) { 208 | var req dto.ConfigRemoveReq 209 | if bindOrAbort(c, &req) != nil { 210 | return 211 | } 212 | 213 | var userID int64 214 | if getUserIDOrAbort(c, &userID) != nil { 215 | return 216 | } 217 | 218 | // check existence and ownership 219 | if configOwnershipOrAbort(c, req.ID, userID) != nil { 220 | return 221 | } 222 | 223 | const sqlCommand string = "update t_config set c_deleted = true where c_id = ?;" 224 | res, err := db.DB.Exec(sqlCommand, req.ID) 225 | if err != nil { 226 | logrus.Error(err) 227 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 228 | return 229 | } 230 | 231 | if affected, _ := res.RowsAffected(); affected > 0 { 232 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.ConfigRemoveRes("ok"))) 233 | } else { 234 | c.JSON(http.StatusBadGateway, dto.NewResponseBad("bad")) 235 | } 236 | } 237 | 238 | // get config's name, remark, create time and modify time owned by user. 239 | // result will be from 'offset', 'count' no more than 30 240 | func configGetList(c *gin.Context) { 241 | var req dto.ConfigGetListReq 242 | if bindOrAbort(c, &req) != nil { 243 | return 244 | } 245 | 246 | var userID int64 247 | if getUserIDOrAbort(c, &userID) != nil { 248 | return 249 | } 250 | 251 | // count no more than 30 252 | if req.Count > 30 { 253 | req.Count = 30 254 | } 255 | 256 | switch req.SortBy { 257 | case "createTime": 258 | req.SortBy = "c_create_time" 259 | case "modifyTime": 260 | req.SortBy = "c_modify_time" 261 | case "name": 262 | req.SortBy = "c_name" 263 | case "id": 264 | req.SortBy = "c_id" 265 | default: 266 | req.SortBy = "c_id" 267 | } 268 | 269 | const sqlCommandPre = "select c_id, c_type, c_name, c_format, c_remark, c_create_time, c_modify_time from t_config" + 270 | " where c_owner_id = ? and c_deleted = false order by %s limit ?, ?;" 271 | var sqlCommand string = fmt.Sprintf(sqlCommandPre, req.SortBy) 272 | rows, err := db.DB.Query(sqlCommand, userID, req.Offset, req.Count) 273 | if err != nil { 274 | logrus.Error(err) 275 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error)) 276 | return 277 | } 278 | 279 | var configSummarys []dto.ConfigSummary = make([]dto.ConfigSummary, 0) 280 | for rows.Next() { 281 | var configSummary dto.ConfigSummary 282 | rows.Scan(&configSummary.ID, 283 | &configSummary.Type, 284 | &configSummary.Name, 285 | &configSummary.Format, 286 | &configSummary.Remark, 287 | &configSummary.CreateTime, 288 | &configSummary.ModifyTime) 289 | 290 | configSummarys = append(configSummarys, configSummary) 291 | } 292 | rows.Close() 293 | 294 | c.JSON(http.StatusOK, 295 | dto.NewResponseFine(dto.ConfigGetListRes{Count: int64(len(configSummarys)), Configs: configSummarys})) 296 | } 297 | 298 | func configShareCreate(c *gin.Context) { 299 | var req dto.ConfigShareCreateReq 300 | if bindOrAbort(c, &req) != nil { 301 | return 302 | } 303 | 304 | var userID int64 305 | if getUserIDOrAbort(c, &userID) != nil { 306 | return 307 | } 308 | 309 | // check existence and ownership 310 | if configOwnershipOrAbort(c, req.ID, userID) != nil { 311 | return 312 | } 313 | 314 | const sqlCommand string = "insert into t_config_share (c_config_id, c_remark) values (?, ?);" 315 | res, err := db.DB.Exec(sqlCommand, req.ID, req.Remark) 316 | if err != nil { 317 | logrus.Error(err) 318 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 319 | return 320 | } 321 | inserted, _ := res.LastInsertId() 322 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.ConfigShareCreateRes{ID: inserted})) 323 | } 324 | 325 | func configShareModify(c *gin.Context) { 326 | var req dto.ConfigShareModifyReq 327 | if bindOrAbort(c, &req) != nil { 328 | return 329 | } 330 | 331 | var userID int64 332 | if getUserIDOrAbort(c, &userID) != nil { 333 | return 334 | } 335 | 336 | // check existence and ownership 337 | if configShareOwnershipOrAbort(c, req.ID, userID) != nil { 338 | return 339 | } 340 | 341 | const sqlCommand = "update t_config_share set c_remark = ? where c_id = ?;" 342 | _, err := db.DB.Exec(sqlCommand, req.Remark, req.ID) 343 | if err != nil { 344 | logrus.Error(err) 345 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 346 | return 347 | } 348 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.ConfigShareModifyRes("ok"))) 349 | } 350 | 351 | func configShareRevoke(c *gin.Context) { 352 | var req dto.ConfigShareRevokeReq 353 | if bindOrAbort(c, &req) != nil { 354 | return 355 | } 356 | 357 | var userID int64 358 | if getUserIDOrAbort(c, &userID) != nil { 359 | return 360 | } 361 | 362 | // check existence and ownership 363 | if configShareOwnershipOrAbort(c, req.ID, userID) != nil { 364 | return 365 | } 366 | 367 | _, err := db.DB.Exec("update t_config_share set c_deleted = true where c_id = ?;", req.ID) 368 | if err != nil { 369 | logrus.Error(err) 370 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 371 | return 372 | } 373 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.ConfigShareRevokeRes("ok"))) 374 | } 375 | 376 | func configShareGetList(c *gin.Context) { 377 | var req dto.ConfigShareGetListReq 378 | if bindOrAbort(c, &req) != nil { 379 | return 380 | } 381 | 382 | var userID int64 383 | if getUserIDOrAbort(c, &userID) != nil { 384 | return 385 | } 386 | 387 | // check existence and ownership 388 | if configOwnershipOrAbort(c, req.ID, userID) != nil { 389 | return 390 | } 391 | 392 | const sqlCommand = "select c_id, c_create_time, c_remark from t_config_share " + 393 | "where c_deleted = false and c_config_id = ?;" 394 | rows, err := db.DB.Query(sqlCommand, req.ID) 395 | defer rows.Close() 396 | if err != nil { 397 | logrus.Error(err) 398 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 399 | return 400 | } 401 | var shareDetails []dto.ConfigShareDetail = make([]dto.ConfigShareDetail, 0) 402 | for rows.Next() { 403 | var shareDetail dto.ConfigShareDetail 404 | if err := rows.Scan(&shareDetail.ID, &shareDetail.CreateTime, &shareDetail.Remark); err != nil { 405 | logrus.Error(err) 406 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 407 | return 408 | } 409 | shareDetails = append(shareDetails, shareDetail) 410 | } 411 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.ConfigShareGetListRes{Shares: shareDetails})) 412 | } 413 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 10 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 11 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 12 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 13 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 14 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 15 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 16 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 17 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 18 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 19 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 20 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 21 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 22 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 23 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 24 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 25 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 26 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 27 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 28 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 29 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 31 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 32 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 33 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 34 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 35 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 36 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 37 | github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= 38 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 39 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 40 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 41 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 42 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 43 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 44 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 45 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 46 | github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= 47 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 49 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 50 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 51 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 52 | github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= 53 | github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= 54 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 55 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 56 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 57 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 58 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 59 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 60 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 61 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 62 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 63 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 65 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 66 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 69 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 70 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 71 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 72 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 73 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 74 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 75 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 76 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 77 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 78 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 79 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 80 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 81 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 82 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 83 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 84 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 85 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 86 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 87 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 88 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 89 | golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= 90 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 91 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 92 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 93 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 94 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 96 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 97 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 100 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 102 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 103 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 104 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 105 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 106 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 107 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 108 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 109 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 110 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 111 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 113 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 114 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 115 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 116 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= 117 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 118 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 119 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 120 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 121 | google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= 122 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 123 | google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= 124 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 125 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 126 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 127 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 128 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 129 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 130 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 131 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 132 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 133 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 134 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 136 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 137 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 138 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 139 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 140 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 141 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 142 | -------------------------------------------------------------------------------- /routers/plan.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/leafee98/class-schedule-to-icalendar-restserver/db" 11 | "github.com/leafee98/class-schedule-to-icalendar-restserver/dto" 12 | "github.com/leafee98/class-schedule-to-icalendar-restserver/utils" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func init() { 17 | RegisterRouter("plan-create", "post", planCreate) 18 | RegisterRouter("plan-add-config", "post", planAddConfig) 19 | RegisterRouter("plan-remove-config", "post", planRemoveConfig) 20 | RegisterRouter("plan-add-share", "post", planAddShare) 21 | RegisterRouter("plan-remove-share", "post", planRemoveShare) 22 | RegisterRouter("plan-get-by-id", "post", planGetById) 23 | RegisterRouter("plan-get-by-share", "post", planGetByShare) 24 | RegisterRouter("plan-remove", "post", planRemove) 25 | RegisterRouter("plan-modify", "post", planModify) 26 | RegisterRouter("plan-get-list", "post", planGetList) 27 | 28 | RegisterRouter("plan-create-token", "post", planCreateToken) 29 | RegisterRouter("plan-revoke-token", "post", planRevokeToken) 30 | RegisterRouter("plan-get-token-list", "post", planGetTokenList) 31 | 32 | RegisterRouter("/plan-share-create", "post", planShareCreate) 33 | RegisterRouter("/plan-share-modify", "post", planShareModify) 34 | RegisterRouter("/plan-share-revoke", "post", planShareRevoke) 35 | RegisterRouter("/plan-share-get-list", "post", planShareGetList) 36 | } 37 | 38 | // create a plan 39 | // only need a Name and Remark, return ID 40 | func planCreate(c *gin.Context) { 41 | // bind parameter 42 | var req dto.PlanCreateReq 43 | if bindOrAbort(c, &req) != nil { 44 | return 45 | } 46 | 47 | // login status 48 | var userID int64 49 | if getUserIDOrAbort(c, &userID) != nil { 50 | return 51 | } 52 | 53 | res, err := db.DB.Exec("insert into t_plan (c_name, c_owner_id, c_remark) values (?, ?, ?);", 54 | req.Name, userID, req.Remark) 55 | if err != nil { 56 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 57 | return 58 | } 59 | 60 | planID, _ := res.LastInsertId() 61 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanCreateRes{ID: planID})) 62 | } 63 | 64 | // check login status 65 | // check if plan & config exist 66 | // check if the relation already exist 67 | // check ownership 68 | // todo: transaction 69 | func planAddConfig(c *gin.Context) { 70 | // bind request 71 | var req dto.PlanAddConfigReq 72 | if bindOrAbort(c, &req) != nil { 73 | return 74 | } 75 | 76 | // check login status 77 | var userID int64 78 | if getUserIDOrAbort(c, &userID) != nil { 79 | return 80 | } 81 | 82 | // check ownership 83 | if planOwnershipOrAbort(c, req.PlanID, userID) != nil { 84 | return 85 | } 86 | if configOwnershipOrAbort(c, req.ConfigID, userID) != nil { 87 | return 88 | } 89 | 90 | // check relation exist 91 | err3 := relationExist(req.PlanID, req.ConfigID) 92 | if err3 == nil { 93 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("this config already added to the plan")) 94 | return 95 | } 96 | 97 | // create relation 98 | _, err := db.DB.Exec("insert into t_plan_config_relation (c_plan_id, c_config_id) values (?, ?)", 99 | req.PlanID, req.ConfigID) 100 | if err != nil { 101 | logrus.Error(err) 102 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 103 | return 104 | } 105 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanAddConfigRes("ok"))) 106 | } 107 | 108 | func planRemoveConfig(c *gin.Context) { 109 | // bind request 110 | var req dto.PlanRemoveConfigReq 111 | if bindOrAbort(c, &req) != nil { 112 | return 113 | } 114 | 115 | // check login status 116 | var userID int64 117 | if getUserIDOrAbort(c, &userID) != nil { 118 | return 119 | } 120 | 121 | // check ownership 122 | if planOwnershipOrAbort(c, req.PlanID, userID) != nil { 123 | return 124 | } 125 | if configOwnershipOrAbort(c, req.ConfigID, userID) != nil { 126 | return 127 | } 128 | 129 | // check relation exist 130 | err3 := relationExist(req.PlanID, req.ConfigID) 131 | if err3 != nil { 132 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("this config haven't been added to the plan")) 133 | return 134 | } 135 | 136 | // remove the relation 137 | const sqlCommand string = `delete from t_plan_config_relation where c_plan_id = ? and c_config_id = ?;` 138 | res, err := db.DB.Exec(sqlCommand, req.PlanID, req.ConfigID) 139 | if err != nil { 140 | logrus.Error(err) 141 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 142 | return 143 | } 144 | 145 | if affected, _ := res.RowsAffected(); affected > 0 { 146 | c.JSON(http.StatusOK, dto.NewResponseFine("ok")) 147 | } else { 148 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad("no relation deleted")) 149 | } 150 | } 151 | 152 | // check login status 153 | // check if plan & config share exist 154 | // check if the relation already exist 155 | // check plan ownership 156 | func planAddShare(c *gin.Context) { 157 | // bind request 158 | var req dto.PlanAddShareReq 159 | if bindOrAbort(c, &req) != nil { 160 | return 161 | } 162 | 163 | // check login status 164 | var userID int64 165 | if getUserIDOrAbort(c, &userID) != nil { 166 | return 167 | } 168 | 169 | // check plan ownership 170 | if planOwnershipOrAbort(c, req.PlanID, userID) != nil { 171 | return 172 | } 173 | // check config share existence 174 | if configShareExistOrAbort(c, req.ConfigShareID) != nil { 175 | return 176 | } 177 | 178 | // check relation exist 179 | err3 := relationShareExist(req.PlanID, req.ConfigShareID) 180 | if err3 == nil { 181 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("this config share already added to the plan")) 182 | return 183 | } 184 | 185 | // create relation 186 | _, err := db.DB.Exec("insert into t_plan_config_share_relation (c_plan_id, c_config_share_id) values (?, ?);", 187 | req.PlanID, req.ConfigShareID) 188 | if err != nil { 189 | logrus.Error(err) 190 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 191 | return 192 | } 193 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanAddConfigRes("ok"))) 194 | } 195 | 196 | func planRemoveShare(c *gin.Context) { 197 | // bind request 198 | var req dto.PlanRemoveShareReq 199 | if bindOrAbort(c, &req) != nil { 200 | return 201 | } 202 | 203 | // check login status 204 | var userID int64 205 | if getUserIDOrAbort(c, &userID) != nil { 206 | return 207 | } 208 | 209 | // check plan ownership 210 | if planOwnershipOrAbort(c, req.PlanID, userID) != nil { 211 | return 212 | } 213 | 214 | // check relation of share exist 215 | err3 := relationShareExist(req.PlanID, req.ConfigShareID) 216 | if err3 != nil { 217 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("this config haven't been added to the plan")) 218 | return 219 | } 220 | 221 | // remove the relation 222 | const sqlCommand string = `delete from t_plan_config_share_relation where c_plan_id = ? and c_config_share_id = ?;` 223 | res, err := db.DB.Exec(sqlCommand, req.PlanID, req.ConfigShareID) 224 | if err != nil { 225 | logrus.Error(err) 226 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 227 | return 228 | } 229 | 230 | if affected, _ := res.RowsAffected(); affected > 0 { 231 | c.JSON(http.StatusOK, dto.NewResponseFine("ok")) 232 | } else { 233 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad("no relation of share deleted")) 234 | } 235 | } 236 | 237 | // check login status 238 | // check plan existence and ownership 239 | func planGetById(c *gin.Context) { 240 | var req dto.PlanGetByIdReq 241 | if bindOrAbort(c, &req) != nil { 242 | return 243 | } 244 | 245 | var userID int64 246 | if getUserIDOrAbort(c, &userID) != nil { 247 | return 248 | } 249 | 250 | if planOwnershipOrAbort(c, req.ID, userID) != nil { 251 | return 252 | } 253 | 254 | var res dto.PlanGetRes 255 | if err := planGetRes(&res, req.ID); err != nil { 256 | logrus.Error(err) 257 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 258 | } else { 259 | c.JSON(http.StatusOK, dto.NewResponseFine(res)) 260 | } 261 | } 262 | 263 | func planGetByShare(c *gin.Context) { 264 | var req dto.PlanGetByShareReq 265 | if bindOrAbort(c, &req) != nil { 266 | return 267 | } 268 | 269 | // get plan id 270 | var planID int64 271 | row := db.DB.QueryRow("select c_plan_id from t_plan_share where c_id = ?;", req.ID) 272 | if err := row.Scan(&planID); err != nil { 273 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("plan share not exist")) 274 | return 275 | } 276 | 277 | var res dto.PlanGetRes 278 | if err := planGetRes(&res, planID); err != nil { 279 | logrus.Error(err) 280 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 281 | } else { 282 | c.JSON(http.StatusOK, dto.NewResponseFine(res)) 283 | } 284 | } 285 | 286 | func planRemove(c *gin.Context) { 287 | var req dto.PlanRemoveReq 288 | if bindOrAbort(c, &req) != nil { 289 | return 290 | } 291 | 292 | var userID int64 293 | if getUserIDOrAbort(c, &userID) != nil { 294 | return 295 | } 296 | 297 | if planOwnershipOrAbort(c, req.ID, userID) != nil { 298 | return 299 | } 300 | 301 | const sqlCommand string = "update t_plan set c_deleted = true where c_id = ?;" 302 | res, err := db.DB.Exec(sqlCommand, req.ID) 303 | if err != nil { 304 | logrus.Error(err) 305 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 306 | return 307 | } 308 | if affected, _ := res.RowsAffected(); affected > 0 { 309 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanRemoveRes("ok"))) 310 | } else { 311 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad("deleted nothing")) 312 | } 313 | } 314 | 315 | func planModify(c *gin.Context) { 316 | var req dto.PlanModifyReq 317 | if bindOrAbort(c, &req) != nil { 318 | return 319 | } 320 | 321 | var userID int64 322 | if getUserIDOrAbort(c, &userID) != nil { 323 | return 324 | } 325 | 326 | if planOwnershipOrAbort(c, req.ID, userID) != nil { 327 | return 328 | } 329 | 330 | const sqlCommand = "update t_plan set c_name = ?, c_remark = ? where c_id = ?;" 331 | _, err := db.DB.Exec(sqlCommand, req.Name, req.Remark, req.ID) 332 | if err != nil { 333 | logrus.Error(err) 334 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 335 | return 336 | } 337 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanRemoveRes("ok"))) 338 | } 339 | 340 | func planGetList(c *gin.Context) { 341 | var req dto.PlanGetListReq 342 | if bindOrAbort(c, &req) != nil { 343 | return 344 | } 345 | 346 | var userID int64 347 | if getUserIDOrAbort(c, &userID) != nil { 348 | return 349 | } 350 | 351 | // count no more than 30 352 | if req.Count > 30 { 353 | req.Count = 30 354 | } 355 | 356 | switch req.SortBy { 357 | case "createTime": 358 | req.SortBy = "c_create_time" 359 | case "modifyTime": 360 | req.SortBy = "c_modify_time" 361 | case "name": 362 | req.SortBy = "c_name" 363 | case "id": 364 | req.SortBy = "c_id" 365 | default: 366 | req.SortBy = "c_id" 367 | } 368 | 369 | const sqlCommandPre = "select c_id, c_name, c_remark, c_create_time, c_modify_time" + 370 | " from t_plan where c_owner_id = ? and c_deleted = false order by %s limit ?, ?;" 371 | var sqlCommand string = fmt.Sprintf(sqlCommandPre, req.SortBy) 372 | rows, err := db.DB.Query(sqlCommand, userID, req.Offset, req.Count) 373 | if err != nil { 374 | logrus.Error(err) 375 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error)) 376 | return 377 | } 378 | 379 | var planSummarys []dto.PlanSummary = make([]dto.PlanSummary, 0) 380 | for rows.Next() { 381 | var planSummary dto.PlanSummary 382 | rows.Scan(&planSummary.ID, 383 | &planSummary.Name, 384 | &planSummary.Remark, 385 | &planSummary.CreateTime, 386 | &planSummary.ModifyTime) 387 | 388 | planSummarys = append(planSummarys, planSummary) 389 | } 390 | rows.Close() 391 | 392 | c.JSON(http.StatusOK, 393 | dto.NewResponseFine(dto.PlanGetListRes{Count: int64(len(planSummarys)), Plans: planSummarys})) 394 | } 395 | 396 | // check delete status 397 | // check ownership 398 | func planCreateToken(c *gin.Context) { 399 | var req dto.PlanCreateTokenReq 400 | if bindOrAbort(c, &req) != nil { 401 | return 402 | } 403 | 404 | var userID int64 405 | if getUserIDOrAbort(c, &userID) != nil { 406 | return 407 | } 408 | 409 | // existence and ownership 410 | if planOwnershipOrAbort(c, req.ID, userID) != nil { 411 | return 412 | } 413 | 414 | // cannot create token of plan more than 30 415 | var tokenCount int64 416 | row := db.DB.QueryRow("select count(*) from t_plan_token where c_plan_id = ?;", req.ID) 417 | err := row.Scan(&tokenCount) 418 | if err != nil { 419 | logrus.Error(err) 420 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 421 | } 422 | if tokenCount >= 30 { 423 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad( 424 | "number of tokens of the same plan cannot be more than 30, revoke some tokens before create more.")) 425 | return 426 | } 427 | 428 | token := utils.GenerateToken() 429 | _, err = db.DB.Exec("insert into t_plan_token (c_plan_id, c_token) values (?, ?)", req.ID, token) 430 | if err != nil { 431 | logrus.Error(err) 432 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 433 | return 434 | } 435 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanCreateTokenRes{Token: token})) 436 | } 437 | 438 | // check login status 439 | // check token existence 440 | // check plan existence and ownership 441 | func planRevokeToken(c *gin.Context) { 442 | var req dto.PlanRevokeTokenReq 443 | if bindOrAbort(c, &req) != nil { 444 | return 445 | } 446 | 447 | var userID int64 448 | if getUserIDOrAbort(c, &userID) != nil { 449 | return 450 | } 451 | 452 | // get planID from token 453 | var planID int64 454 | row := db.DB.QueryRow("select c_plan_id from t_plan_token where c_token = ?;", req.Token) 455 | err := row.Scan(&planID) 456 | if err == sql.ErrNoRows { 457 | c.AbortWithStatusJSON(http.StatusBadRequest, dto.NewResponseBad("no such token")) 458 | return 459 | } else if err != nil { 460 | logrus.Error(err) 461 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 462 | return 463 | } 464 | 465 | if planOwnershipOrAbort(c, planID, userID) != nil { 466 | return 467 | } 468 | 469 | // delete this token 470 | res, err := db.DB.Exec("delete from t_plan_token where c_token = ?;", req.Token) 471 | if err != nil { 472 | logrus.Error(err) 473 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 474 | return 475 | } 476 | if affected, _ := res.RowsAffected(); affected > 0 { 477 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanRevokeTokenRes("ok"))) 478 | } else { 479 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(dto.PlanRevokeTokenRes("deleted nothing"))) 480 | } 481 | } 482 | 483 | // check login status 484 | // check plan existence and ownership 485 | func planGetTokenList(c *gin.Context) { 486 | var req dto.PlanGetTokenListReq 487 | if bindOrAbort(c, &req) != nil { 488 | return 489 | } 490 | 491 | var userID int64 492 | if getUserIDOrAbort(c, &userID) != nil { 493 | return 494 | } 495 | 496 | if planOwnershipOrAbort(c, req.ID, userID) != nil { 497 | return 498 | } 499 | 500 | rows, err := db.DB.Query("select c_token, c_create_time from t_plan_token where c_plan_id = ?;", req.ID) 501 | if err != nil { 502 | logrus.Error(err) 503 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 504 | return 505 | } 506 | var tokens []dto.PlanTokenDetail = make([]dto.PlanTokenDetail, 0) 507 | var token dto.PlanTokenDetail 508 | for rows.Next() { 509 | rows.Scan(&token.Token, &token.CreateTime) 510 | tokens = append(tokens, token) 511 | } 512 | rows.Close() 513 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanGetTokenListRes{Count: int64(len(tokens)), Tokens: tokens})) 514 | } 515 | 516 | func planShareCreate(c *gin.Context) { 517 | var req dto.PlanShareCreateReq 518 | if bindOrAbort(c, &req) != nil { 519 | return 520 | } 521 | 522 | var userID int64 523 | if getUserIDOrAbort(c, &userID) != nil { 524 | return 525 | } 526 | 527 | // check existence and ownership 528 | if planOwnershipOrAbort(c, req.ID, userID) != nil { 529 | return 530 | } 531 | 532 | const sqlCommand string = "insert into t_plan_share (c_plan_id, c_remark) values (?, ?);" 533 | res, err := db.DB.Exec(sqlCommand, req.ID, req.Remark) 534 | if err != nil { 535 | logrus.Error(err) 536 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 537 | return 538 | } 539 | inserted, _ := res.LastInsertId() 540 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanShareCreateRes{ID: inserted})) 541 | } 542 | 543 | func planShareModify(c *gin.Context) { 544 | var req dto.PlanShareModifyReq 545 | if bindOrAbort(c, &req) != nil { 546 | return 547 | } 548 | 549 | var userID int64 550 | if getUserIDOrAbort(c, &userID) != nil { 551 | return 552 | } 553 | 554 | // check existence and ownership 555 | if planShareOwnershipOrAbort(c, req.ID, userID) != nil { 556 | return 557 | } 558 | 559 | const sqlCommand = "update t_plan_share set c_remark = ? where c_id = ?;" 560 | _, err := db.DB.Exec(sqlCommand, req.Remark, req.ID) 561 | if err != nil { 562 | logrus.Error(err) 563 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 564 | return 565 | } 566 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanShareModifyRes("ok"))) 567 | } 568 | 569 | func planShareRevoke(c *gin.Context) { 570 | var req dto.PlanShareRevokeReq 571 | if bindOrAbort(c, &req) != nil { 572 | return 573 | } 574 | 575 | var userID int64 576 | if getUserIDOrAbort(c, &userID) != nil { 577 | return 578 | } 579 | 580 | // check existence and ownership 581 | if planShareOwnershipOrAbort(c, req.ID, userID) != nil { 582 | return 583 | } 584 | 585 | _, err := db.DB.Exec("update t_plan_share set c_deleted = true where c_id = ?;", req.ID) 586 | if err != nil { 587 | logrus.Error(err) 588 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 589 | return 590 | } 591 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanShareRevokeRes("ok"))) 592 | } 593 | 594 | func planShareGetList(c *gin.Context) { 595 | var req dto.PlanShareGetListReq 596 | if bindOrAbort(c, &req) != nil { 597 | return 598 | } 599 | 600 | var userID int64 601 | if getUserIDOrAbort(c, &userID) != nil { 602 | return 603 | } 604 | 605 | // check existence and ownership 606 | if planOwnershipOrAbort(c, req.ID, userID) != nil { 607 | return 608 | } 609 | 610 | const sqlCommand = "select c_id, c_create_time, c_remark from t_plan_share " + 611 | "where c_deleted = false and c_plan_id = ?;" 612 | rows, err := db.DB.Query(sqlCommand, req.ID) 613 | defer rows.Close() 614 | if err != nil { 615 | logrus.Error(err) 616 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 617 | return 618 | } 619 | var shareDetails []dto.PlanShareDetail = make([]dto.PlanShareDetail, 0) 620 | for rows.Next() { 621 | var shareDetail dto.PlanShareDetail 622 | if err := rows.Scan(&shareDetail.ID, &shareDetail.CreateTime, &shareDetail.Remark); err != nil { 623 | logrus.Error(err) 624 | c.AbortWithStatusJSON(http.StatusBadGateway, dto.NewResponseBad(err.Error())) 625 | return 626 | } 627 | shareDetails = append(shareDetails, shareDetail) 628 | } 629 | c.JSON(http.StatusOK, dto.NewResponseFine(dto.PlanShareGetListRes{Shares: shareDetails})) 630 | } 631 | 632 | //////////////////////////////////////////////// 633 | /////////////////// Utilities ////////////////// 634 | //////////////////////////////////////////////// 635 | 636 | func planGetRes(plan *dto.PlanGetRes, planID int64) error { 637 | plan.Configs = make([]dto.ConfigDetail, 0) 638 | plan.Shares = make([]dto.ConfigDetail, 0) 639 | 640 | const sqlCommandGetPlan string = "select c_id, c_name, c_remark, c_create_time, c_modify_time " + 641 | "from t_plan where c_deleted = false and c_id = ?;" 642 | row := db.DB.QueryRow(sqlCommandGetPlan, planID) 643 | if err := row.Scan(&plan.ID, &plan.Name, &plan.Remark, &plan.CreateTime, &plan.ModifyTime); err != nil { 644 | if err == sql.ErrNoRows { 645 | return errors.New("plan not exist") 646 | } else { 647 | return err 648 | } 649 | } 650 | 651 | const sqlCommandGetConfigs = "select " + 652 | "c_id, c_type, c_name, c_content, c_format, c_remark, c_create_time, c_modify_time " + 653 | "from t_config where c_deleted = false and c_id in " + 654 | "(select c_config_id from t_plan_config_relation where c_plan_id = ?);" 655 | rows, err := db.DB.Query(sqlCommandGetConfigs, planID) 656 | defer rows.Close() 657 | if err != nil { 658 | return err 659 | } 660 | 661 | for rows.Next() { 662 | var co dto.ConfigDetail 663 | err = rows.Scan(&co.ID, &co.Type, &co.Name, &co.Content, &co.Format, &co.Remark, &co.CreateTime, &co.ModifyTime) 664 | if err != nil { 665 | return err 666 | } 667 | plan.Configs = append(plan.Configs, co) 668 | } 669 | 670 | const sqlCommandGetShares = "" + 671 | "select s.c_id, c.c_type, c.c_name, c.c_content, c.c_format, c.c_remark, c.c_create_time, c.c_modify_time " + 672 | "from " + 673 | " t_config as c " + 674 | " join t_config_share as s " + 675 | " on c.c_id = s.c_config_id " + 676 | "where " + 677 | " c.c_deleted = false " + 678 | " and s.c_deleted = false " + 679 | " and s.c_id in ( " + 680 | " select c_config_share_id " + 681 | " from t_plan_config_share_relation " + 682 | " where c_plan_id = ? " + 683 | " );" 684 | rowsShare, err := db.DB.Query(sqlCommandGetShares, planID) 685 | defer rowsShare.Close() 686 | if err != nil { 687 | return err 688 | } 689 | 690 | for rowsShare.Next() { 691 | var cs dto.ConfigDetail 692 | err = rowsShare.Scan(&cs.ID, &cs.Type, &cs.Name, &cs.Content, &cs.Format, &cs.Remark, &cs.CreateTime, 693 | &cs.ModifyTime) 694 | if err != nil { 695 | return err 696 | } 697 | plan.Shares = append(plan.Shares, cs) 698 | } 699 | 700 | return nil 701 | } 702 | --------------------------------------------------------------------------------