diff --git a/apiV1PayoutEx.go b/apiV1PayoutEx.go new file mode 100644 index 0000000..b2b5b80 --- /dev/null +++ b/apiV1PayoutEx.go @@ -0,0 +1,59 @@ +package main + +import ( + "biukop.com/sfm/loan" + log "github.com/sirupsen/logrus" + "net/http" + "strconv" +) + +func apiV1PayOutExGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + strId := r.URL.Path[len(apiV1Prefix+"payout-ex/"):] + ret := loan.PayOutEx{} + id, e := strconv.Atoi(strId) + if e != nil { + log.Error("invalid id for PayOutEx", strId, e.Error()) + apiV1Client403Error(w, r, ss) + } + e = ret.Read(int64(id)) + if e != nil { + log.Error("failed to read PayOutEx", strId, e.Error()) + apiV1Server500Error(w, r) + } + + apiV1SendJson(ret, w, r, ss) + return +} + +func apiV1PayOutExListGet(w http.ResponseWriter, r *http.Request, ss *loan.Session) { + // strId := r.URL.Path[len(apiV1Prefix+"payout-ex-list/"):] + input, e := decodeJsonFullLoanOverview(r) + + // TODO; Generalize that input, and return a list of filters and sort + + // Todo: do some serious query for the data in the Grid, and make it a pattern. Get it done! + + if e != nil { + apiV1EmptyResponse(w, r, ss) + } else { + + switch ss.GetRole() { + case "broker": + input.Filter.Filters = append(input.Filter.Filters, loan.FullLoanSummaryFilter{ + Field: "broker_ids", + Operator: "contains", + Value: loan.JsonString(ss.User)}) + break + case "user": + input.Filter.Filters = append(input.Filter.Filters, loan.FullLoanSummaryFilter{ + Field: "client_ids", + Operator: "contains", + Value: loan.JsonString(ss.User)}) + break + } + + data := loan.QFullLLoanSummary(input) + //send out + apiV1SendJson(data, w, r, ss) + } +} diff --git a/apiV1PeopleList.go b/apiV1PeopleList.go index 10c0ade..688baab 100644 --- a/apiV1PeopleList.go +++ b/apiV1PeopleList.go @@ -107,7 +107,7 @@ func apiV1PeopleExtraGet(w http.ResponseWriter, r *http.Request, ss *loan.Sessio id := r.URL.Path[len(apiV1Prefix+"people-extra/"):] ret := UserExtra{} ret.Role = loan.GetRoleById(id) - + //TODO; check manager and account role switch ret.Role { case "people": apiV1SendJson(ret, w, r, ss) diff --git a/apiV1logout.go b/apiV1logout.go index 1b7b690..e5eed7f 100644 --- a/apiV1logout.go +++ b/apiV1logout.go @@ -11,10 +11,8 @@ func apiV1Logout(w http.ResponseWriter, r *http.Request, ss *loan.Session) { res := apiV1ResponseBlank() WsNotifyLogout(ss) ss.Expire = time.Now().Add(-10000) //make sure it expired - - ssEmpty := loan.Session{} log.Info("Logout user ", ss.User, " from session ", ss.Id) //send out - apiV1AddTrackingCookie(w, r, &ssEmpty) //always the last one to set cookies - res.sendJson(w) + apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies + _, _ = res.sendJson(w) } diff --git a/apiv1.go b/apiv1.go index 67ff3ed..87919c3 100644 --- a/apiv1.go +++ b/apiv1.go @@ -77,7 +77,6 @@ func setupApiV1Handler() []apiV1HandlerMap { {"GET", "login-available/", apiV1LoginAvailable}, {"POST", "lender-upload/", apiV1UploadsPost}, - {"GET", "lender-upload/", apiV1UploadOriginalFileGet}, {"GET", "upload-analysis/", apiV1UploadAnalysis}, {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis}, @@ -94,6 +93,8 @@ func setupApiV1Handler() []apiV1HandlerMap { {"GET", "lender-list/", apiV1LenderList}, + {"GET", "payout-ex/", apiV1PayOutExGet}, + {"GET", "login", apiV1DumpRequest}, } } else { //production @@ -148,7 +149,6 @@ func setupApiV1Handler() []apiV1HandlerMap { {"GET", "login-available/", apiV1LoginAvailable}, {"POST", "lender-upload/", apiV1UploadsPost}, - {"GET", "lender-upload/", apiV1UploadOriginalFileGet}, {"GET", "upload-analysis/", apiV1UploadAnalysis}, {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis}, @@ -165,6 +165,8 @@ func setupApiV1Handler() []apiV1HandlerMap { {"GET", "lender-list/", apiV1LenderList}, + {"GET", "payout-ex/", apiV1PayOutExGet}, + {"GET", "login", apiV1EmptyResponse}, } } @@ -188,43 +190,69 @@ func apiV1Main(w http.ResponseWriter, r *http.Request) { logRequestDebug(httputil.DumpRequest(r, true)) } - session := apiV1InitSession(r) - if config.Debug { - log.Debugf("session : %+v", session) + session := loan.Session{} + session.MarkEmpty() + bypassSession := apiV1NoNeedSession(r) + if !bypassSession { // no point to create session for preflight + session = apiV1InitSession(r) + if config.Debug { + log.Debugf("session : %+v", session) + } } //search through handler path := r.URL.Path[len(apiV1Prefix):] //strip API prefix + handled := false for _, node := range apiV1Handler { - //log.Println(node, path, strings.HasPrefix(path, node.Path)) - if (r.Method == node.Method || node.Method == "*") && strings.HasPrefix(path, node.Path) { + if (strings.ToUpper(r.Method) == node.Method || node.Method == "*") && strings.HasPrefix(path, node.Path) { + handled = true node.Handler(w, r, &session) - e := session.Write() //finish this session to DB - if e != nil { - log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error()) - } - return + break //stop search handler further + } + } + + if !bypassSession { // no point to write session for preflight + e := session.Write() //finish this session to DB + if e != nil { + log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error()) } } //Catch for all UnHandled Request - e := session.Write() //finish this session to DB - if e != nil { - log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error()) + if !handled { + if config.Debug { + apiV1DumpRequest(w, r, &session) + } else { + apiV1EmptyResponse(w, r, &session) + } } - if config.Debug { - apiV1DumpRequest(w, r, &session) - } else { - apiV1EmptyResponse(w, r, &session) +} + +func apiV1NoNeedSession(r *http.Request) bool { + if r.Method == "OPTIONS" { + return true } + path := r.URL.Path[len(apiV1Prefix):] //strip API prefix + + if r.Method == "GET" { + if strings.HasPrefix(path, "avatar/") || + strings.HasPrefix(path, "upload-original/") || + strings.HasPrefix(path, "upload-as-image/") || + strings.HasPrefix(path, "upload-as-analysis/") || + strings.HasPrefix(path, "upload-as-thumbnail/") || + strings.HasPrefix(path, "upload-as-pdf/") { + return true + } + } + return false } func apiV1InitSession(r *http.Request) (session loan.Session) { session.MarkEmpty() //track browser, and take session from cookie - cookieSession, e := apiV1InitSessionByBrowserId(r) + cookieSession, e := apiV1InitSessionByCookie(r) if e == nil { session = cookieSession } @@ -255,8 +283,8 @@ func setupCrossOriginResponse(w *http.ResponseWriter, r *http.Request) { method := r.Header.Get("Access-Control-Request-Method") (*w).Header().Set("Access-Control-Allow-Origin", origin) //for that specific origin (*w).Header().Set("Access-Control-Allow-Credentials", "true") - (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, "+method) - (*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Session-Token, Biukop-Session-Expire, "+requestedHeaders) + (*w).Header().Set("Access-Control-Allow-Methods", removeDupHeaderOptions("POST, GET, OPTIONS, PUT, DELETE, "+method)) + (*w).Header().Set("Access-Control-Allow-Headers", removeDupHeaderOptions("Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Session-Token, Biukop-Session-Expire, "+requestedHeaders)) } func apiV1GetMachineId(r *http.Request) string { @@ -275,11 +303,35 @@ func apiV1GetMachineId(r *http.Request) string { return mid } +func apiV1GetCORSHeaders(r *http.Request) (ret string) { + requestedHeaders := r.Header.Get("Access-control-Request-Headers") + ret = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Session-Token, Biukop-Session-Expire," + requestedHeaders + return removeDupHeaderOptions(ret) +} + +func removeDupHeaderOptions(inStr string) (out string) { + headers := map[string]struct{}{} + strings.ReplaceAll(inStr, " ", "") // remove space + headerArray := strings.Split(inStr, ",") // split + for _, v := range headerArray { + headers[v] = struct{}{} // same key will overwrite each other + } + out = "" + for k, _ := range headers { + if out != "" { + out += ", " + } + out += k + } + return +} + func apiV1GetMachineIdFromSession(ss *loan.Session) string { return ss.GetStr("Biukop-Mid") } -func apiV1InitSessionByBrowserId(r *http.Request) (session loan.Session, e error) { +func apiV1InitSessionByCookie(r *http.Request) (session loan.Session, e error) { var sid string + session.MarkEmpty() mid := apiV1GetMachineId(r) inCookie, e := r.Cookie("Biukop-Session") if e == nil { @@ -297,29 +349,60 @@ func apiV1InitSessionByBrowserId(r *http.Request) (session loan.Session, e error } func apiV1AddTrackingCookie(w http.ResponseWriter, r *http.Request, session *loan.Session) { - //set session header too. - w.Header().Add("Access-Control-Expose-Headers", "Biukop-Session") + if strings.ToUpper(r.Method) == "OPTION" { + return + } + w.Header().Add("Access-Control-Expose-Headers", apiV1GetCORSHeaders(r)) + apiV1AddTrackingSession(w, r, session) + apiV1AddTrackingMachineId(w, r, session) +} + +func apiV1AddTrackingSession(w http.ResponseWriter, r *http.Request, session *loan.Session) { + sessionId := "" if session == nil { + log.Warn("non-exist session, empty Id sent", session) w.Header().Add("Biukop-Session", "") } else { - w.Header().Add("Biukop-Session", session.Id) + if session.Id == "" { + log.Warn("empty session, empty Id sent", session) + } else { + w.Header().Add("Biukop-Session", session.Id) + sessionId = session.Id + } + } - //add tracking cookie - expiration := time.Now().Add(365 * 24 * time.Hour) + cookie := http.Cookie{ + Name: "Biukop-Session", + Value: sessionId, // may be "" + Expires: time.Now().Add(365 * 24 * time.Hour), + Path: "/", + Secure: true, + SameSite: http.SameSiteNoneMode} + http.SetCookie(w, &cookie) + +} +func apiV1AddTrackingMachineId(w http.ResponseWriter, r *http.Request, session *loan.Session) { mid := apiV1GetMachineId(r) - cookie := http.Cookie{Name: "Biukop-Mid", Value: mid, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode} + expiration := time.Now().Add(365 * 24 * time.Hour) + + w.Header().Add("Biukop-Mid", mid) + + cookie := http.Cookie{ + Name: "Biukop-Mid", + Value: mid, + Expires: expiration, + Path: "/", + Secure: true, + SameSite: http.SameSiteNoneMode} http.SetCookie(w, &cookie) - if session != nil { - cookie = http.Cookie{Name: "Biukop-Session", Value: session.Id, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode} - http.SetCookie(w, &cookie) - } } func apiV1InitSessionByHttpHeader(r *http.Request) (ss loan.Session, e error) { - sid := r.Header.Get("Biukop-Session") ss.MarkEmpty() + sid := apiV1GetSessionIdFromRequest(r) + //make sure session id is given if sid != "" { e = ss.Retrieve(r) @@ -332,13 +415,20 @@ func apiV1InitSessionByHttpHeader(r *http.Request) (ss loan.Session, e error) { e = errors.New("session not found: " + sid) } return - return } -func apiV1ErrorCheck(e error) { - if nil != e { - panic(e.Error()) //TODO: detailed error check, truck all caller +func apiV1GetSessionIdFromRequest(r *http.Request) string { + sid := "" + inCookie, e := r.Cookie("Biukop-Session") + if e == nil { + sid = inCookie.Value + } + + headerSid := r.Header.Get("Biukop-Session") + if headerSid != "" { + sid = headerSid } + return sid } func apiV1Server500Error(w http.ResponseWriter, r *http.Request) { diff --git a/websocket.go b/websocket.go index d0ea717..4588104 100644 --- a/websocket.go +++ b/websocket.go @@ -25,11 +25,8 @@ func apiV1WebSocketHandler(w http.ResponseWriter, r *http.Request) { return } - mid := apiV1GetMachineId(r) - wsq.MapMidToConnection(mid, ws) - // helpful log statement to show connections - log.Println("Websocket Api/V1: Client Connected", mid) + log.Println("Websocket Api/V1: Client Connected", r.RemoteAddr) wsReader(ws) } @@ -50,21 +47,62 @@ func wsReader(conn *websocket.Conn) { log.Println(err) return } - // print out that message for clarity - // fmt.Println(string(p)) - if err := conn.WriteMessage(messageType, p); err != nil { - wsq.del(conn) - log.Println(err) - return + // WsEchoIncomingMessage(conn, string(p), messageType) + switch messageType { + case websocket.TextMessage: + WsProcessingTxtMessage(conn, string(p)) + break + case websocket.BinaryMessage: + WsProcessingBinaryMessage(conn, p) + break + case websocket.PingMessage: + break + case websocket.PongMessage: + break } - if string(p) == "send dummy string for 500 times" { - go wsDummySender(conn) + } +} + +func WsProcessingTxtMessage(conn *websocket.Conn, str string) { + if str == "send dummy string for 500 times" { + go wsDummySender(conn) + } + + // must be json message with key, value pairs + incoming := make(map[string]string) + e := json.Unmarshal([]byte(str), &incoming) + if e != nil { + log.Error("ws incoming msg error not json", str, e) + return + } + + key := incoming["t"] + for _, v := range apiV1WsHandler { + if key == v.Keyword { + v.Handler(conn, incoming) + return // we have already processed } } } +func WsProcessingBinaryMessage(conn *websocket.Conn, data []byte) { + return +} + +func WsEchoIncomingMessage(conn *websocket.Conn, msg string, messageType int) { + // print out that message for clarity + fmt.Println(msg) + + // this is echo + if err := conn.WriteMessage(messageType, []byte(msg)); err != nil { + wsq.del(conn) + log.Println(err) + return + } +} + func WsBroadCast(msg string) { wsq.wsDoBroadcast(msg, "") } diff --git a/websocket_incoming.go b/websocket_incoming.go new file mode 100644 index 0000000..33f4792 --- /dev/null +++ b/websocket_incoming.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "github.com/gorilla/websocket" + log "github.com/sirupsen/logrus" +) + +type apiV1WsHandlerMap struct { + Keyword string // keyword + Handler func(conn *websocket.Conn, msg map[string]string) +} + +var apiV1WsHandler = []apiV1WsHandlerMap{ + {"echo", apiV1WsEcho}, + {"register-session", apiV1WsRegisterSession}, +} + +func apiV1WsEcho(conn *websocket.Conn, msg map[string]string) { + // print out that message for clarity + fmt.Println(msg) + + // this is echo + if err := conn.WriteMessage(websocket.TextMessage, []byte(msg["t"])); err != nil { + wsq.del(conn) + log.Println(err) + } +} + +func apiV1WsRegisterSession(conn *websocket.Conn, msg map[string]string) { + sid := msg["session"] + wsq.MapSessionToConnection(sid, conn) + log.Info("ws register session:", sid) +} diff --git a/websocket_message.go b/websocket_message.go index c9b30d1..90921c8 100644 --- a/websocket_message.go +++ b/websocket_message.go @@ -1,9 +1,11 @@ package main -import "biukop.com/sfm/loan" +import ( + "biukop.com/sfm/loan" + log "github.com/sirupsen/logrus" +) func WsNotifyNewLogin(ss *loan.Session) { - msg := make(map[string]string) msg["T"] = "login" msg["Mid"] = apiV1GetMachineIdFromSession(ss) @@ -14,7 +16,6 @@ func WsNotifyNewLogin(ss *loan.Session) { } func WsNotifyLogout(ss *loan.Session) { - msg := make(map[string]string) msg["T"] = "logout" msg["Mid"] = apiV1GetMachineIdFromSession(ss) @@ -22,4 +23,5 @@ func WsNotifyLogout(ss *loan.Session) { msg["Sid"] = ss.Id msg["Role"] = ss.GetRole() WsBroadCastExceptMe(ss.Id, msg) + log.Info(ss, msg) } diff --git a/websocket_queue.go b/websocket_queue.go index 6d0106b..bf7b05d 100644 --- a/websocket_queue.go +++ b/websocket_queue.go @@ -7,24 +7,25 @@ import ( type wsQ struct { wsMutex sync.Mutex - Clients map[*websocket.Conn]struct{} + Clients map[*websocket.Conn]string ClientsBySession map[string]*websocket.Conn } var wsq = wsQ{ - Clients: make(map[*websocket.Conn]struct{}), + Clients: make(map[*websocket.Conn]string), ClientsBySession: make(map[string]*websocket.Conn), } func (m *wsQ) addConnection(c *websocket.Conn) { m.wsMutex.Lock() - m.Clients[c] = struct{}{} + m.Clients[c] = "" m.wsMutex.Unlock() } -func (m *wsQ) MapMidToConnection(mid string, c *websocket.Conn) { +func (m *wsQ) MapSessionToConnection(sid string, c *websocket.Conn) { m.wsMutex.Lock() - m.ClientsBySession[mid] = c + m.ClientsBySession[sid] = c + m.Clients[c] = sid m.wsMutex.Unlock() } @@ -50,24 +51,18 @@ func (m *wsQ) getConnBySessionId(sid string) (conn *websocket.Conn) { func (m *wsQ) del(conn *websocket.Conn) { m.wsMutex.Lock() - delete(m.Clients, conn) - for k, v := range m.ClientsBySession { - if v == conn { - delete(m.ClientsBySession, k) - break - } + if sid, exist := m.Clients[conn]; exist { + delete(m.Clients, conn) + delete(m.ClientsBySession, sid) } m.wsMutex.Unlock() } func (m *wsQ) delBySessionId(sid string) { m.wsMutex.Lock() - for k, v := range m.ClientsBySession { - if k == sid { - delete(m.ClientsBySession, k) - break - } - delete(m.Clients, v) + if conn, exist := m.ClientsBySession[sid]; exist { + delete(m.ClientsBySession, sid) + delete(m.Clients, conn) } m.wsMutex.Unlock() }