選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

420 行
13KB

  1. package main
  2. import (
  3. "biukop.com/sfm/loan"
  4. "database/sql"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "github.com/brianvoe/gofakeit/v6"
  9. log "github.com/sirupsen/logrus"
  10. "net/http"
  11. "net/http/httputil"
  12. "strings"
  13. "time"
  14. )
  15. const apiV1Prefix = "/api/v1/"
  16. const apiV1WebSocket = apiV1Prefix + "ws"
  17. type apiV1HandlerMap struct {
  18. Method string
  19. Path string //regex
  20. Handler func(http.ResponseWriter, *http.Request, *loan.Session)
  21. }
  22. var apiV1Handler = setupApiV1Handler()
  23. func setupApiV1Handler() []apiV1HandlerMap {
  24. if config.Debug { //debug only
  25. return []apiV1HandlerMap{
  26. {"POST", "login", apiV1Login},
  27. {"*", "logout", apiV1Logout},
  28. {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans},
  29. {"GET", "chart/amount-of-loans", apiV1ChartTypeOfLoans},
  30. {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly},
  31. {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans},
  32. {"GET", "chart/top-broker", apiV1ChartTopBroker},
  33. {"POST", "grid/loan/full-loan-overview", apiV1GridLoanFullOverview},
  34. {"GET", "chart/reward-vs-income-monthly", apiV1ChartRewardVsIncomeMonthly},
  35. {"GET", "loan/", apiV1LoanSingleGet},
  36. {"DELETE", "loan/", apiV1LoanSingleDelete},
  37. {"GET", "loan-by-client/", apiV1LoanByClient},
  38. {"GET", "people/", apiV1PeopleGet},
  39. {"POST", "people/", apiV1PeoplePost},
  40. {"PUT", "people/", apiV1PeoplePut},
  41. {"DELETE", "people/", apiV1PeopleDelete},
  42. {"GET", "people-extra/", apiV1PeopleExtraGet},
  43. {"POST", "user/", apiV1UserPost},
  44. {"PUT", "user/", apiV1UserPut},
  45. {"DELETE", "user/", apiV1UserDelete},
  46. {"POST", "user-enable/", apiV1UserEnable},
  47. {"GET", "broker/", apiV1BrokerGet},
  48. {"POST", "broker/", apiV1BrokerPost},
  49. {"PUT", "broker/", apiV1BrokerPut},
  50. {"DELETE", "broker/", apiV1BrokerDelete},
  51. {"POST", "change-pass/", apiV1ChangePass},
  52. {"POST", "loan/basic/", apiV1LoanSinglePostBasic},
  53. {"GET", "avatar/", apiV1Avatar},
  54. {"POST", "avatar/", apiV1AvatarPost},
  55. {"POST", "reward/", apiV1RewardPost},
  56. {"DELETE", "reward/", apiV1RewardDelete},
  57. {"GET", "people-list/", apiV1PeopleList},
  58. {"GET", "broker-list/", apiV1BrokerList},
  59. {"POST", "sync-people/", apiV1SyncPeople},
  60. {"POST", "payIn/", apiV1PayInPost},
  61. {"DELETE", "payIn/", apiV1PayInDelete},
  62. {"POST", "pay-in-list/", apiV1PayInList},
  63. {"GET", "user-reward/", apiV1UserReward},
  64. {"GET", "login-available/", apiV1LoginAvailable},
  65. {"POST", "lender-upload/", apiV1UploadsPost},
  66. {"GET", "lender-upload/", apiV1UploadOriginalFileGet},
  67. {"GET", "upload-analysis/", apiV1UploadAnalysis},
  68. {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis},
  69. {"GET", "upload-as-image/", apiV1UploadAsImage},
  70. {"PUT", "upload-as-image/", apiV1UploadCreateImage},
  71. {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail},
  72. {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail},
  73. {"GET", "upload-as-pdf/", apiV1UploadAsPDF},
  74. {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF},
  75. {"GET", "upload-original/", apiV1UploadOriginalFileGet},
  76. {"GET", "upload/", apiV1UploadMetaGet},
  77. {"DELETE", "upload/", apiV1UploadDelete},
  78. {"POST", "upload-meta-list/", apiV1UploadMetaList},
  79. {"GET", "lender-list/", apiV1LenderList},
  80. {"GET", "login", apiV1DumpRequest},
  81. }
  82. } else { //production
  83. return []apiV1HandlerMap{
  84. {"POST", "login", apiV1Login},
  85. {"*", "logout", apiV1Logout},
  86. {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans},
  87. {"GET", "chart/amount-of-loans", apiV1ChartTypeOfLoans},
  88. {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly},
  89. {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans},
  90. {"GET", "chart/top-broker", apiV1ChartTopBroker},
  91. {"POST", "grid/loan/full-loan-overview", apiV1GridLoanFullOverview},
  92. {"GET", "chart/reward-vs-income-monthly", apiV1ChartRewardVsIncomeMonthly},
  93. {"GET", "loan/", apiV1LoanSingleGet},
  94. {"DELETE", "loan/", apiV1LoanSingleDelete},
  95. {"GET", "loan-by-client/", apiV1LoanByClient},
  96. {"GET", "people/", apiV1PeopleGet},
  97. {"POST", "people/", apiV1PeoplePost},
  98. {"PUT", "people/", apiV1PeoplePut},
  99. {"DELETE", "people/", apiV1PeopleDelete},
  100. {"GET", "people-extra/", apiV1PeopleExtraGet},
  101. {"POST", "user/", apiV1UserPost},
  102. {"PUT", "user/", apiV1UserPut},
  103. {"DELETE", "user/", apiV1UserDelete},
  104. {"POST", "user-enable/", apiV1UserEnable},
  105. {"GET", "broker/", apiV1BrokerGet},
  106. {"POST", "broker/", apiV1BrokerPost},
  107. {"PUT", "broker/", apiV1BrokerPut},
  108. {"DELETE", "broker/", apiV1BrokerDelete},
  109. {"POST", "change-pass/", apiV1ChangePass},
  110. {"POST", "loan/basic/", apiV1LoanSinglePostBasic},
  111. {"GET", "avatar/", apiV1Avatar},
  112. {"POST", "avatar/", apiV1AvatarPost},
  113. {"POST", "reward/", apiV1RewardPost},
  114. {"DELETE", "reward/", apiV1RewardDelete},
  115. {"GET", "people-list", apiV1PeopleList},
  116. {"GET", "broker-list/", apiV1BrokerList},
  117. {"POST", "sync-people/", apiV1SyncPeople},
  118. {"POST", "payIn/", apiV1PayInPost},
  119. {"DELETE", "payIn/", apiV1PayInDelete},
  120. {"POST", "pay-in-list/", apiV1PayInList},
  121. {"GET", "user-reward/", apiV1UserReward},
  122. {"GET", "login-available/", apiV1LoginAvailable},
  123. {"POST", "lender-upload/", apiV1UploadsPost},
  124. {"GET", "lender-upload/", apiV1UploadOriginalFileGet},
  125. {"GET", "upload-analysis/", apiV1UploadAnalysis},
  126. {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis},
  127. {"GET", "upload-as-image/", apiV1UploadAsImage},
  128. {"PUT", "upload-as-image/", apiV1UploadCreateImage},
  129. {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail},
  130. {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail},
  131. {"GET", "upload-as-pdf/", apiV1UploadAsPDF},
  132. {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF},
  133. {"GET", "upload-original/", apiV1UploadOriginalFileGet},
  134. {"GET", "upload-meta/", apiV1UploadMetaGet},
  135. {"DELETE", "upload/", apiV1UploadDelete},
  136. {"POST", "upload-meta-list/", apiV1UploadMetaList},
  137. {"GET", "lender-list/", apiV1LenderList},
  138. {"GET", "login", apiV1EmptyResponse},
  139. }
  140. }
  141. }
  142. //
  143. //apiV1Main version 1 main entry for all REST API
  144. //
  145. func apiV1Main(w http.ResponseWriter, r *http.Request) {
  146. //CORS setup
  147. setupCrossOriginResponse(&w, r)
  148. //if its options then we don't bother with other issues
  149. if r.Method == "OPTIONS" {
  150. apiV1EmptyResponse(w, r, nil)
  151. return //stop processing
  152. }
  153. if config.Debug {
  154. logRequestDebug(httputil.DumpRequest(r, true))
  155. }
  156. session := apiV1InitSession(r)
  157. if config.Debug {
  158. log.Debugf("session : %+v", session)
  159. }
  160. //search through handler
  161. path := r.URL.Path[len(apiV1Prefix):] //strip API prefix
  162. for _, node := range apiV1Handler {
  163. //log.Println(node, path, strings.HasPrefix(path, node.Path))
  164. if (r.Method == node.Method || node.Method == "*") && strings.HasPrefix(path, node.Path) {
  165. node.Handler(w, r, &session)
  166. e := session.Write() //finish this session to DB
  167. if e != nil {
  168. log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error())
  169. }
  170. return
  171. }
  172. }
  173. //Catch for all UnHandled Request
  174. e := session.Write() //finish this session to DB
  175. if e != nil {
  176. log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error())
  177. }
  178. if config.Debug {
  179. apiV1DumpRequest(w, r, &session)
  180. } else {
  181. apiV1EmptyResponse(w, r, &session)
  182. }
  183. }
  184. func apiV1InitSession(r *http.Request) (session loan.Session) {
  185. session.MarkEmpty()
  186. //track browser, and take session from cookie
  187. cookieSession, e := apiV1InitSessionByBrowserId(r)
  188. if e == nil {
  189. session = cookieSession
  190. }
  191. //try session login first, if not an empty session will be created
  192. headerSession, e := apiV1InitSessionByHttpHeader(r)
  193. if e == nil {
  194. session = headerSession
  195. }
  196. if session.IsEmpty() {
  197. session.InitGuest(time.Now().Add(loan.DefaultSessionDuration))
  198. } else {
  199. session.RenewIfExpireSoon()
  200. }
  201. //we have a session anyway
  202. session.Add("Biukop-Mid", apiV1GetMachineId(r)) //set machine id
  203. session.SetRemote(r) //make sure they are using latest remote
  204. return
  205. }
  206. func setupCrossOriginResponse(w *http.ResponseWriter, r *http.Request) {
  207. origin := r.Header.Get("Origin")
  208. if origin == "" {
  209. origin = "*"
  210. }
  211. requestedHeaders := r.Header.Get("Access-control-Request-Headers")
  212. method := r.Header.Get("Access-Control-Request-Method")
  213. (*w).Header().Set("Access-Control-Allow-Origin", origin) //for that specific origin
  214. (*w).Header().Set("Access-Control-Allow-Credentials", "true")
  215. (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, "+method)
  216. (*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)
  217. }
  218. func apiV1GetMachineId(r *http.Request) string {
  219. var mid string
  220. inCookie, e := r.Cookie("Biukop-Mid")
  221. if e == nil {
  222. mid = inCookie.Value
  223. } else {
  224. mid = gofakeit.UUID()
  225. }
  226. headerMid := r.Header.Get("Biukop-Mid")
  227. if headerMid != "" {
  228. mid = headerMid
  229. }
  230. return mid
  231. }
  232. func apiV1InitSessionByBrowserId(r *http.Request) (session loan.Session, e error) {
  233. var sid string
  234. mid := apiV1GetMachineId(r)
  235. inCookie, e := r.Cookie("Biukop-Session")
  236. if e == nil {
  237. sid = inCookie.Value
  238. if sid != "" {
  239. e = session.Read(sid)
  240. if e == nil {
  241. if mid != session.Get("Biukop-Mid") {
  242. session.MarkEmpty()
  243. }
  244. }
  245. }
  246. }
  247. return
  248. }
  249. func apiV1AddTrackingCookie(w http.ResponseWriter, r *http.Request, session *loan.Session) {
  250. //set session header too.
  251. w.Header().Add("Access-Control-Expose-Headers", "Biukop-Session")
  252. if session == nil {
  253. w.Header().Add("Biukop-Session", "")
  254. } else {
  255. w.Header().Add("Biukop-Session", session.Id)
  256. }
  257. //add tracking cookie
  258. expiration := time.Now().Add(365 * 24 * time.Hour)
  259. mid := apiV1GetMachineId(r)
  260. cookie := http.Cookie{Name: "Biukop-Mid", Value: mid, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode}
  261. http.SetCookie(w, &cookie)
  262. if session != nil {
  263. cookie = http.Cookie{Name: "Biukop-Session", Value: session.Id, Expires: expiration, Path: "/", Secure: true, SameSite: http.SameSiteNoneMode}
  264. http.SetCookie(w, &cookie)
  265. }
  266. }
  267. func apiV1InitSessionByHttpHeader(r *http.Request) (ss loan.Session, e error) {
  268. sid := r.Header.Get("Biukop-Session")
  269. ss.MarkEmpty()
  270. //make sure session id is given
  271. if sid != "" {
  272. e = ss.Retrieve(r)
  273. if e == sql.ErrNoRows { //db does not have required session.
  274. log.Warn("DB has no corresponding session ", sid)
  275. } else if e != nil { // retrieve DB has error
  276. log.Errorf("Retrieve Session %s encountered error %s", sid, e.Error())
  277. }
  278. } else {
  279. e = errors.New("session not found: " + sid)
  280. }
  281. return
  282. return
  283. }
  284. func apiV1ErrorCheck(e error) {
  285. if nil != e {
  286. panic(e.Error()) //TODO: detailed error check, truck all caller
  287. }
  288. }
  289. func apiV1Server500Error(w http.ResponseWriter, r *http.Request) {
  290. //general setup
  291. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  292. w.WriteHeader(500)
  293. apiV1AddTrackingCookie(w, r, nil) //always the last one to set cookies
  294. fmt.Fprintf(w, "Server Internal Error "+time.Now().Format(time.RFC1123))
  295. //write log
  296. dump := logRequestDebug(httputil.DumpRequest(r, true))
  297. dump = strings.TrimSpace(dump)
  298. log.Warnf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path)
  299. }
  300. func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  301. //general setup
  302. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  303. w.WriteHeader(403)
  304. type struct403 struct {
  305. Error int
  306. ErrorMsg string
  307. }
  308. e403 := struct403{Error: 403, ErrorMsg: "Not Authorized " + time.Now().Format(time.RFC1123)}
  309. msg403, _ := json.Marshal(e403)
  310. //before send out
  311. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  312. fmt.Fprintln(w, string(msg403))
  313. //write log
  314. dump := logRequestDebug(httputil.DumpRequest(r, true))
  315. dump = strings.TrimSpace(dump)
  316. log.Warnf("Not authorized http(%s) path= %s, %s", r.Method, r.URL.Path, dump)
  317. }
  318. func apiV1Client404Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  319. //general setup
  320. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  321. w.WriteHeader(404)
  322. type struct404 struct {
  323. Error int
  324. ErrorMsg string
  325. }
  326. e404 := struct404{Error: 404, ErrorMsg: "Not Found " + time.Now().Format(time.RFC1123)}
  327. msg404, _ := json.Marshal(e404)
  328. //before send out
  329. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  330. fmt.Fprintln(w, string(msg404))
  331. //write log
  332. dump := logRequestDebug(httputil.DumpRequest(r, true))
  333. dump = strings.TrimSpace(dump)
  334. log.Warnf("Not found http(%s) path= %s, %s", r.Method, r.URL.Path, dump)
  335. }
  336. func apiV1DumpRequest(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  337. dump := logRequestDebug(httputil.DumpRequest(r, true))
  338. dump = strings.TrimSpace(dump)
  339. msg := fmt.Sprintf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path)
  340. dumpLines := strings.Split(dump, "\r\n")
  341. ar := apiV1ResponseBlank()
  342. ar.Env.Msg = msg
  343. ar.Env.Session = *ss
  344. ar.Env.Session.Bin = []byte("masked data") //clear
  345. ar.Env.Session.Secret = "***********"
  346. ar.add("Body", dumpLines)
  347. ar.add("Biukop-Mid", ss.Get("Biukop-Mid"))
  348. b, _ := ar.toJson()
  349. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  350. fmt.Fprintf(w, "%s\n", b)
  351. }
  352. func apiV1EmptyResponse(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  353. //general setup
  354. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  355. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  356. fmt.Fprintf(w, "")
  357. }