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

572 行
18KB

  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", apiV1ChartAmountOfLoans},
  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. {"POST", "grid/loan/full-loan-overview", apiV1GridListLoanOverview},
  35. {"GET", "chart/reward-vs-income-monthly", apiV1ChartRewardVsIncomeMonthly},
  36. {"GET", "loan/", apiV1LoanSingleGet},
  37. {"DELETE", "loan/", apiV1LoanSingleDelete},
  38. {"GET", "loan-by-client/", apiV1LoanByClient},
  39. {"GET", "people/", apiV1PeopleGet},
  40. {"POST", "people/", apiV1PeoplePost},
  41. {"PUT", "people/", apiV1PeoplePut},
  42. {"DELETE", "people/", apiV1PeopleDelete},
  43. {"GET", "people-extra/", apiV1PeopleExtraGet},
  44. {"POST", "user/", apiV1UserPost},
  45. {"PUT", "user/", apiV1UserPut},
  46. {"DELETE", "user/", apiV1UserDelete},
  47. {"POST", "user-enable/", apiV1UserEnable},
  48. {"GET", "user-ex/", apiV1UserExGet},
  49. {"POST", "user-ex/", apiV1UserExPost},
  50. {"GET", "user-ex-list/", apiV1UserExList},
  51. {"GET", "broker/", apiV1BrokerGet},
  52. {"POST", "broker/", apiV1BrokerPost},
  53. {"PUT", "broker/", apiV1BrokerPut},
  54. {"DELETE", "broker/", apiV1BrokerDelete},
  55. {"POST", "change-pass/", apiV1ChangePass},
  56. {"POST", "loan/basic/", apiV1LoanSinglePostBasic},
  57. {"GET", "avatar/", apiV1Avatar},
  58. {"POST", "avatar/", apiV1AvatarPost},
  59. {"POST", "reward/", apiV1RewardPost},
  60. {"DELETE", "reward/", apiV1RewardDelete},
  61. {"GET", "people-list/", apiV1PeopleList},
  62. {"GET", "broker-list/", apiV1BrokerList},
  63. {"POST", "sync-people/", apiV1SyncPeople},
  64. {"POST", "payIn/", apiV1PayInPost},
  65. {"DELETE", "payIn/", apiV1PayInDelete},
  66. {"POST", "pay-in-list/", apiV1PayInList},
  67. {"POST", "pay-in-filtered-list/", apiV1FilteredPayInList},
  68. {"GET", "user-reward/", apiV1UserReward},
  69. {"GET", "login-available/", apiV1LoginAvailable},
  70. {"POST", "lender-upload/", apiV1UploadsPost},
  71. {"GET", "upload-analysis/", apiV1UploadAnalysis},
  72. {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis},
  73. {"GET", "upload-as-image/", apiV1UploadAsImage},
  74. {"PUT", "upload-as-image/", apiV1UploadCreateImage},
  75. {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail},
  76. {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail},
  77. {"GET", "upload-as-pdf/", apiV1UploadAsPDF},
  78. {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF},
  79. {"GET", "upload-original/", apiV1UploadOriginalFileGet},
  80. {"GET", "upload/", apiV1UploadMetaGet},
  81. {"DELETE", "upload/", apiV1UploadDelete},
  82. {"POST", "upload-meta-list/", apiV1UploadMetaList},
  83. {"GET", "lender-list/", apiV1LenderList},
  84. {"GET", "payout-ex/", apiV1PayOutExGet},
  85. {"POST", "reward-ex-list/", apiV1RewardExListPost},
  86. {"POST", "payout/", apiV1PayOutPost},
  87. {"POST", "payout-prepared/", apiV1PayOutPrepared},
  88. {"POST", "payout-unprepared/", apiV1PayOutUnprepared},
  89. {"POST", "payout-approved/", apiV1PayOutApproved},
  90. {"POST", "payout-unapproved/", apiV1PayOutUnapproved},
  91. {"POST", "payout-paid/", apiV1PayOutPaid},
  92. {"POST", "payout-unpaid/", apiV1PayOutUnpaid},
  93. {"POST", "step-upload/", apiV1StepUploadsPost},
  94. {"GET", "step-download/", apiV1StepUploadsDownload},
  95. {"POST", "step-id/", apiV1StepUploadsGetID},
  96. {"POST", "step-meta-update/", apiV1StepUploadsMetaPost},
  97. {"DELETE", "step/", apiV1StepDelete},
  98. {"DELETE", "step-file/", apiV1StepDeleteFile},
  99. {"POST", "email-password/", apiV1EmailPassword},
  100. {"GET", "login", apiV1DumpRequest},
  101. }
  102. } else { //production
  103. return []apiV1HandlerMap{
  104. {"POST", "login", apiV1Login},
  105. {"*", "logout", apiV1Logout},
  106. {"GET", "chart/type-of-loans", apiV1ChartTypeOfLoans},
  107. {"GET", "chart/amount-of-loans", apiV1ChartAmountOfLoans},
  108. {"GET", "chart/past-year-monthly", apiV1ChartPastYearMonthly},
  109. {"GET", "chart/recent-10-loans", apiV1ChartRecent10Loans},
  110. {"GET", "chart/top-broker", apiV1ChartTopBroker},
  111. //{"POST", "grid/loan/full-loan-overview", apiV1GridLoanFullOverview},
  112. {"POST", "grid/loan/full-loan-overview", apiV1GridListLoanOverview},
  113. {"GET", "chart/reward-vs-income-monthly", apiV1ChartRewardVsIncomeMonthly},
  114. {"GET", "loan/", apiV1LoanSingleGet},
  115. {"DELETE", "loan/", apiV1LoanSingleDelete},
  116. {"GET", "loan-by-client/", apiV1LoanByClient},
  117. {"GET", "people/", apiV1PeopleGet},
  118. {"POST", "people/", apiV1PeoplePost},
  119. {"PUT", "people/", apiV1PeoplePut},
  120. {"DELETE", "people/", apiV1PeopleDelete},
  121. {"GET", "people-extra/", apiV1PeopleExtraGet},
  122. {"POST", "user/", apiV1UserPost},
  123. {"PUT", "user/", apiV1UserPut},
  124. {"DELETE", "user/", apiV1UserDelete},
  125. {"POST", "user-enable/", apiV1UserEnable},
  126. {"GET", "user-ex/", apiV1UserExGet},
  127. {"POST", "user-ex/", apiV1UserExPost},
  128. {"GET", "user-ex-list/", apiV1UserExList},
  129. {"GET", "broker/", apiV1BrokerGet},
  130. {"POST", "broker/", apiV1BrokerPost},
  131. {"PUT", "broker/", apiV1BrokerPut},
  132. {"DELETE", "broker/", apiV1BrokerDelete},
  133. {"POST", "change-pass/", apiV1ChangePass},
  134. {"POST", "loan/basic/", apiV1LoanSinglePostBasic},
  135. {"GET", "avatar/", apiV1Avatar},
  136. {"POST", "avatar/", apiV1AvatarPost},
  137. {"POST", "reward/", apiV1RewardPost},
  138. {"DELETE", "reward/", apiV1RewardDelete},
  139. {"GET", "people-list", apiV1PeopleList},
  140. {"GET", "broker-list/", apiV1BrokerList},
  141. {"POST", "sync-people/", apiV1SyncPeople},
  142. {"POST", "payIn/", apiV1PayInPost},
  143. {"DELETE", "payIn/", apiV1PayInDelete},
  144. {"POST", "pay-in-list/", apiV1PayInList},
  145. {"POST", "pay-in-filtered-list/", apiV1FilteredPayInList},
  146. {"GET", "user-reward/", apiV1UserReward},
  147. {"GET", "login-available/", apiV1LoginAvailable},
  148. {"POST", "lender-upload/", apiV1UploadsPost},
  149. {"GET", "upload-analysis/", apiV1UploadAnalysis},
  150. {"PUT", "upload-analysis/", apiV1UploadCreateAnalysis},
  151. {"GET", "upload-as-image/", apiV1UploadAsImage},
  152. {"PUT", "upload-as-image/", apiV1UploadCreateImage},
  153. {"GET", "upload-as-thumbnail/", apiV1UploadAsThumbnail},
  154. {"PUT", "upload-as-thumbnail/", apiV1UploadCreateThumbnail},
  155. {"GET", "upload-as-pdf/", apiV1UploadAsPDF},
  156. {"PUT", "upload-as-pdf/", apiV1UploadCreatePDF},
  157. {"GET", "upload-original/", apiV1UploadOriginalFileGet},
  158. {"GET", "upload-meta/", apiV1UploadMetaGet},
  159. {"DELETE", "upload/", apiV1UploadDelete},
  160. {"POST", "upload-meta-list/", apiV1UploadMetaList},
  161. {"GET", "lender-list/", apiV1LenderList},
  162. {"GET", "payout-ex/", apiV1PayOutExGet},
  163. {"POST", "reward-ex-list/", apiV1RewardExListPost},
  164. {"POST", "payout/", apiV1PayOutPost},
  165. {"POST", "payout-prepared/", apiV1PayOutPrepared},
  166. {"POST", "payout-unprepared/", apiV1PayOutUnprepared},
  167. {"POST", "payout-approved/", apiV1PayOutApproved},
  168. {"POST", "payout-unapproved/", apiV1PayOutUnapproved},
  169. {"POST", "payout-paid/", apiV1PayOutPaid},
  170. {"POST", "payout-unpaid/", apiV1PayOutUnpaid},
  171. {"POST", "step-upload/", apiV1StepUploadsPost},
  172. {"GET", "step-download/", apiV1StepUploadsDownload},
  173. {"POST", "step-id/", apiV1StepUploadsGetID},
  174. {"POST", "step-meta-update/", apiV1StepUploadsMetaPost},
  175. {"DELETE", "step/", apiV1StepDelete},
  176. {"DELETE", "step-file/", apiV1StepDeleteFile},
  177. {"POST", "email-password/", apiV1EmailPassword},
  178. {"GET", "login", apiV1EmptyResponse},
  179. }
  180. }
  181. }
  182. //
  183. //apiV1Main version 1 main entry for all REST API
  184. //
  185. func apiV1Main(w http.ResponseWriter, r *http.Request) {
  186. //CORS setup
  187. setupCrossOriginResponse(&w, r)
  188. //if its options then we don't bother with other issues
  189. if r.Method == "OPTIONS" {
  190. apiV1EmptyResponse(w, r, nil)
  191. return //stop processing
  192. }
  193. if config.Debug {
  194. logRequestDebug(httputil.DumpRequest(r, true))
  195. }
  196. session := loan.Session{}
  197. session.MarkEmpty()
  198. bypassSession := apiV1NoNeedSession(r)
  199. if !bypassSession { // no point to create session for preflight
  200. session = apiV1InitSession(r)
  201. if config.Debug {
  202. log.Debugf("session : %+v", session)
  203. }
  204. }
  205. //search through handler
  206. path := r.URL.Path[len(apiV1Prefix):] //strip API prefix
  207. handled := false
  208. for _, node := range apiV1Handler {
  209. if (strings.ToUpper(r.Method) == node.Method || node.Method == "*") && strings.HasPrefix(path, node.Path) {
  210. handled = true
  211. node.Handler(w, r, &session)
  212. break //stop search handler further
  213. }
  214. }
  215. if !bypassSession { // no point to write session for preflight
  216. e := session.Write() //finish this session to DB
  217. if e != nil {
  218. log.Warnf("Failed to Save Session %+v \n reason \n%s\n", session, e.Error())
  219. }
  220. }
  221. //Catch for all UnHandled Request
  222. if !handled {
  223. if config.Debug {
  224. apiV1DumpRequest(w, r, &session)
  225. } else {
  226. apiV1EmptyResponse(w, r, &session)
  227. }
  228. }
  229. }
  230. func apiV1NoNeedSession(r *http.Request) bool {
  231. if r.Method == "OPTIONS" {
  232. return true
  233. }
  234. path := r.URL.Path[len(apiV1Prefix):] //strip API prefix
  235. if r.Method == "GET" {
  236. if strings.HasPrefix(path, "avatar/") ||
  237. strings.HasPrefix(path, "upload-original/") ||
  238. strings.HasPrefix(path, "upload-as-image/") ||
  239. strings.HasPrefix(path, "upload-as-analysis/") ||
  240. strings.HasPrefix(path, "upload-as-thumbnail/") ||
  241. strings.HasPrefix(path, "upload-as-pdf/") {
  242. return true
  243. }
  244. }
  245. return false
  246. }
  247. func apiV1InitSession(r *http.Request) (session loan.Session) {
  248. session.MarkEmpty()
  249. //try session login first, if not an empty session will be created
  250. headerSession, e := apiV1InitSessionByHttpHeader(r)
  251. if e == nil {
  252. session = headerSession
  253. } else { // if session from header failed, we try cookie session
  254. // track browser, and take session from cookie
  255. // cookie has disadvantage that multiple tab will get overwritten
  256. cookieSession, e := apiV1InitSessionByCookie(r)
  257. if e == nil {
  258. session = cookieSession
  259. }
  260. }
  261. if session.IsEmpty() {
  262. session.InitGuest(time.Now().Add(loan.DefaultSessionDuration))
  263. } else {
  264. session.RenewIfExpireSoon()
  265. }
  266. //we have a session anyway
  267. session.Add("Biukop-Mid", apiV1GetMachineId(r)) //set machine id
  268. session.Add("Biukop-Socket", apiV1GetSocketId(r)) //set machine id
  269. session.SetRemote(r) //make sure they are using latest remote
  270. return
  271. }
  272. func setupCrossOriginResponse(w *http.ResponseWriter, r *http.Request) {
  273. origin := r.Header.Get("Origin")
  274. if origin == "" {
  275. origin = "*"
  276. }
  277. requestedHeaders := r.Header.Get("Access-control-Request-Headers")
  278. method := r.Header.Get("Access-Control-Request-Method")
  279. (*w).Header().Set("Access-Control-Allow-Origin", origin) //for that specific origin
  280. (*w).Header().Set("Access-Control-Allow-Credentials", "true")
  281. (*w).Header().Set("Access-Control-Allow-Methods", removeDupHeaderOptions("POST, GET, OPTIONS, PUT, DELETE, "+method))
  282. (*w).Header().Set("Access-Control-Allow-Headers", removeDupHeaderOptions("Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Socket , "+requestedHeaders))
  283. }
  284. func apiV1GetMachineId(r *http.Request) string {
  285. var mid string
  286. inCookie, e := r.Cookie("Biukop-Mid")
  287. if e == nil {
  288. mid = inCookie.Value
  289. } else {
  290. mid = gofakeit.UUID()
  291. }
  292. headerMid := r.Header.Get("Biukop-Mid")
  293. if headerMid != "" {
  294. mid = headerMid
  295. }
  296. return mid
  297. }
  298. func apiV1GetSocketId(r *http.Request) string {
  299. socketId := r.Header.Get("Biukop-Socket")
  300. return socketId
  301. }
  302. func apiV1GetCORSHeaders(r *http.Request) (ret string) {
  303. requestedHeaders := r.Header.Get("Access-control-Request-Headers")
  304. ret = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Cookie, Biukop-Session, Biukop-Session-Token, Biukop-Session-Expire," + requestedHeaders
  305. return removeDupHeaderOptions(ret)
  306. }
  307. func removeDupHeaderOptions(inStr string) (out string) {
  308. headers := map[string]struct{}{}
  309. strings.ReplaceAll(inStr, " ", "") // remove space
  310. headerArray := strings.Split(inStr, ",") // split
  311. for _, v := range headerArray {
  312. headers[v] = struct{}{} // same key will overwrite each other
  313. }
  314. out = ""
  315. for k, _ := range headers {
  316. if out != "" {
  317. out += ", "
  318. }
  319. out += k
  320. }
  321. return
  322. }
  323. func apiV1GetMachineIdFromSession(ss *loan.Session) string {
  324. return ss.GetStr("Biukop-Mid")
  325. }
  326. func apiV1InitSessionByCookie(r *http.Request) (session loan.Session, e error) {
  327. var sid string
  328. session.MarkEmpty()
  329. mid := apiV1GetMachineId(r)
  330. inCookie, e := r.Cookie("Biukop-Session")
  331. if e == nil {
  332. sid = inCookie.Value
  333. if sid != "" {
  334. e = session.Read(sid)
  335. if e == nil {
  336. if mid != session.Get("Biukop-Mid") {
  337. session.MarkEmpty()
  338. }
  339. }
  340. }
  341. }
  342. return
  343. }
  344. func apiV1AddTrackingCookie(w http.ResponseWriter, r *http.Request, session *loan.Session) {
  345. if strings.ToUpper(r.Method) == "OPTION" {
  346. return
  347. }
  348. w.Header().Add("Access-Control-Expose-Headers", apiV1GetCORSHeaders(r))
  349. apiV1AddTrackingSession(w, r, session)
  350. apiV1AddTrackingMachineId(w, r, session)
  351. }
  352. func apiV1AddTrackingSession(w http.ResponseWriter, r *http.Request, session *loan.Session) {
  353. sessionId := ""
  354. if session == nil {
  355. log.Warn("non-exist session, empty Id sent", session)
  356. // w.Header().Add("Biukop-Session", "")
  357. } else {
  358. if session.Id == "" {
  359. log.Warn("empty session, empty Id sent", session)
  360. } else {
  361. w.Header().Add("Biukop-Session", session.Id)
  362. sessionId = session.Id
  363. }
  364. }
  365. cookie := http.Cookie{
  366. Name: "Biukop-Session",
  367. Value: sessionId, // may be ""
  368. Expires: time.Now().Add(365 * 24 * time.Hour),
  369. Path: "/",
  370. Secure: true,
  371. SameSite: http.SameSiteNoneMode}
  372. http.SetCookie(w, &cookie)
  373. }
  374. func apiV1AddTrackingMachineId(w http.ResponseWriter, r *http.Request, session *loan.Session) {
  375. mid := apiV1GetMachineId(r)
  376. expiration := time.Now().Add(365 * 24 * time.Hour)
  377. w.Header().Add("Biukop-Mid", mid)
  378. cookie := http.Cookie{
  379. Name: "Biukop-Mid",
  380. Value: mid,
  381. Expires: expiration,
  382. Path: "/",
  383. Secure: true,
  384. SameSite: http.SameSiteNoneMode}
  385. http.SetCookie(w, &cookie)
  386. }
  387. func apiV1InitSessionByHttpHeader(r *http.Request) (ss loan.Session, e error) {
  388. ss.MarkEmpty()
  389. sid := apiV1GetSessionIdFromRequest(r)
  390. //make sure session id is given
  391. if sid != "" {
  392. e = ss.Retrieve(r)
  393. if e == sql.ErrNoRows { //db does not have required session.
  394. log.Warn("DB has no corresponding session ", sid)
  395. } else if e != nil { // retrieve DB has error
  396. log.Errorf("Retrieve Session %s encountered error %s", sid, e.Error())
  397. }
  398. } else {
  399. e = errors.New("session not found: " + sid)
  400. }
  401. return
  402. }
  403. func apiV1GetSessionIdFromRequest(r *http.Request) string {
  404. sid := ""
  405. inCookie, e := r.Cookie("Biukop-Session")
  406. if e == nil {
  407. sid = inCookie.Value
  408. }
  409. headerSid := r.Header.Get("Biukop-Session")
  410. if headerSid != "" {
  411. sid = headerSid
  412. }
  413. return sid
  414. }
  415. func apiV1Server500Error(w http.ResponseWriter, r *http.Request) {
  416. //general setup
  417. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  418. w.WriteHeader(500)
  419. apiV1AddTrackingCookie(w, r, nil) //always the last one to set cookies
  420. fmt.Fprintf(w, "Server Internal Error "+time.Now().Format(time.RFC1123))
  421. //write log
  422. dump := logRequestDebug(httputil.DumpRequest(r, true))
  423. dump = strings.TrimSpace(dump)
  424. log.Warnf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path)
  425. }
  426. func apiV1Client403Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  427. //general setup
  428. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  429. w.WriteHeader(403)
  430. type struct403 struct {
  431. Error int
  432. ErrorMsg string
  433. }
  434. e403 := struct403{Error: 403, ErrorMsg: "Not Authorized " + time.Now().Format(time.RFC1123)}
  435. msg403, _ := json.Marshal(e403)
  436. //before send out
  437. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  438. fmt.Fprintln(w, string(msg403))
  439. //write log
  440. dump := logRequestDebug(httputil.DumpRequest(r, true))
  441. dump = strings.TrimSpace(dump)
  442. log.Warnf("Not authorized http(%s) path= %s, %s", r.Method, r.URL.Path, dump)
  443. }
  444. func apiV1Client404Error(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  445. //general setup
  446. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  447. w.WriteHeader(404)
  448. type struct404 struct {
  449. Error int
  450. ErrorMsg string
  451. }
  452. e404 := struct404{Error: 404, ErrorMsg: "Not Found " + time.Now().Format(time.RFC1123)}
  453. msg404, _ := json.Marshal(e404)
  454. //before send out
  455. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  456. fmt.Fprintln(w, string(msg404))
  457. //write log
  458. dump := logRequestDebug(httputil.DumpRequest(r, true))
  459. dump = strings.TrimSpace(dump)
  460. log.Warnf("Not found http(%s) path= %s, %s", r.Method, r.URL.Path, dump)
  461. }
  462. func apiV1DumpRequest(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  463. dump := logRequestDebug(httputil.DumpRequest(r, true))
  464. dump = strings.TrimSpace(dump)
  465. msg := fmt.Sprintf("Unhandled Protocol = %s path= %s", r.Method, r.URL.Path)
  466. dumpLines := strings.Split(dump, "\r\n")
  467. ar := apiV1ResponseBlank()
  468. ar.Env.Msg = msg
  469. ar.Env.Session = *ss
  470. ar.Env.Session.Bin = []byte("masked data") //clear
  471. ar.Env.Session.Secret = "***********"
  472. ar.add("Body", dumpLines)
  473. ar.add("Biukop-Mid", ss.Get("Biukop-Mid"))
  474. b, _ := ar.toJson()
  475. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  476. fmt.Fprintf(w, "%s\n", b)
  477. }
  478. func apiV1EmptyResponse(w http.ResponseWriter, r *http.Request, ss *loan.Session) {
  479. //general setup
  480. w.Header().Set("Content-Type", "application/json;charset=UTF-8")
  481. apiV1AddTrackingCookie(w, r, ss) //always the last one to set cookies
  482. fmt.Fprintf(w, "")
  483. }