1 | package rest |
||
2 | |||
3 | import ( |
||
4 | "bufio" |
||
5 | "log" |
||
6 | "net/http" |
||
7 | "os" |
||
8 | "path/filepath" |
||
9 | "strings" |
||
10 | |||
11 | "github.com/go-chi/chi" |
||
12 | "github.com/go-chi/chi/middleware" |
||
13 | "github.com/go-chi/render" |
||
14 | |||
15 | "encoding/json" |
||
16 | "fmt" |
||
17 | "github.com/popstas/go-toggl" |
||
18 | "github.com/popstas/planfix-go/planfix" |
||
19 | "github.com/viasite/planfix-toggl-server/app/client" |
||
20 | "github.com/viasite/planfix-toggl-server/app/config" |
||
21 | "time" |
||
22 | ) |
||
23 | |||
24 | // Server is a rest with store |
||
25 | type Server struct { |
||
26 | Version string |
||
27 | TogglClient *client.TogglClient |
||
28 | Config *config.Config |
||
29 | Logger *log.Logger |
||
30 | } |
||
31 | |||
32 | //Run the lister and request's router, activate rest server |
||
33 | func (s Server) Run(portSSL int) { |
||
34 | //port := 8096 |
||
35 | //s.Logger.Printf("[INFO] запуск сервера на :%d", port) |
||
36 | |||
37 | router := chi.NewRouter() |
||
38 | router.Use(middleware.RealIP, Recoverer) |
||
39 | router.Use(AppInfo("planfix-toggl", s.Version), Ping) |
||
40 | router.Use(CORS) |
||
41 | |||
42 | router.Route("/api/v1", func(r chi.Router) { |
||
43 | if s.Config.Debug { |
||
44 | r.Use(Logger()) |
||
45 | } |
||
46 | |||
47 | // toggl |
||
48 | r.Route("/toggl", func(r chi.Router) { |
||
49 | r.Get("/entries", s.getEntriesCtrl) |
||
50 | r.Get("/entries/current", s.getTogglCurrentCtrl) |
||
51 | r.Get("/entries/planfix/{taskID}", s.getPlanfixTaskCtrl) |
||
52 | r.Get("/entries/planfix/{taskID}/last", s.getPlanfixTaskLastCtrl) |
||
53 | r.Get("/user", s.getTogglUser) |
||
54 | r.Get("/workspaces", s.getTogglWorkspaces) |
||
55 | }) |
||
56 | |||
57 | // config |
||
58 | r.Route("/config", func(r chi.Router) { |
||
59 | r.Get("/", s.getConfigCtrl) |
||
60 | r.Options("/", s.updateConfigCtrl) |
||
61 | r.Post("/", s.updateConfigCtrl) |
||
62 | r.Post("/reload", s.reloadConfigCtrl) |
||
63 | }) |
||
64 | |||
65 | // planfix |
||
66 | r.Route("/planfix", func(r chi.Router) { |
||
67 | r.Get("/user", s.getPlanfixUser) |
||
68 | r.Get("/analitics", s.getPlanfixAnalitics) |
||
69 | }) |
||
70 | |||
71 | // validate |
||
72 | r.Route("/validate", func(r chi.Router) { |
||
73 | r.Get("/config", s.validateConfig) |
||
74 | r.Get("/planfix/user", s.validatePlanfixUser) |
||
75 | r.Get("/planfix/analitic", s.validatePlanfixAnalitic) |
||
76 | r.Get("/toggl/user", s.validateTogglUser) |
||
77 | r.Get("/toggl/workspace", s.validateTogglWorkspace) |
||
78 | }) |
||
79 | }) |
||
80 | |||
81 | router.Get("/robots.txt", func(w http.ResponseWriter, r *http.Request) { |
||
82 | render.PlainText(w, r, "User-agent: *\nDisallow: /api/") |
||
83 | }) |
||
84 | |||
85 | router.Get("/log", func(w http.ResponseWriter, r *http.Request) { |
||
86 | log := "" |
||
87 | |||
88 | file, err := os.Open(s.TogglClient.Config.LogFile) |
||
89 | if err != nil { |
||
90 | render.Status(r, 400) |
||
91 | render.PlainText(w, r, err.Error()) |
||
92 | } |
||
93 | defer file.Close() |
||
94 | |||
95 | fileScanner := bufio.NewScanner(file) |
||
96 | |||
97 | for fileScanner.Scan() { |
||
98 | line := fileScanner.Text() |
||
99 | line = strings.Replace(line, "[planfix-toggl]", "", -1) |
||
100 | log += fmt.Sprintf("%s<br>", line) |
||
101 | } |
||
102 | |||
103 | render.HTML(w, r, log) |
||
104 | }) |
||
105 | |||
106 | s.fileServer(router, "/", http.Dir(filepath.Join(".", "docroot"))) |
||
107 | |||
108 | //go http.ListenAndServe(fmt.Sprintf(":%d", port), router) |
||
109 | s.Logger.Printf("[INFO] веб-интерфейс на https://localhost:%d", portSSL) |
||
110 | s.Logger.Println(http.ListenAndServeTLS( |
||
111 | fmt.Sprintf(":%d", portSSL), |
||
112 | "certs/server.crt", |
||
113 | "certs/server.key", router), |
||
114 | ) |
||
115 | |||
116 | //s.Logger.Printf("[INFO] веб-интерфейс на http://localhost:%d", port) |
||
117 | //s.Logger.Println(http.ListenAndServe(fmt.Sprintf(":%d", port), router)) |
||
118 | } |
||
119 | |||
120 | // GET /v1/toggl/entries |
||
121 | func (s Server) getEntriesCtrl(w http.ResponseWriter, r *http.Request) { |
||
122 | var entries []client.TogglPlanfixEntry |
||
123 | var err error |
||
124 | queryValues := r.URL.Query() |
||
125 | t := queryValues.Get("type") |
||
126 | tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02") |
||
127 | |||
128 | if t == "today" { |
||
129 | entries, err = s.TogglClient.GetEntries( |
||
130 | s.Config.TogglWorkspaceID, |
||
131 | time.Now().Format("2006-01-02"), |
||
132 | tomorrow, |
||
133 | ) |
||
134 | } else if t == "pending" { |
||
135 | entries, err = s.TogglClient.GetPendingEntries() |
||
136 | } else if t == "last" { |
||
137 | var pageEntries []client.TogglPlanfixEntry |
||
138 | for currentPage := 1; currentPage <= 10; currentPage++ { |
||
139 | pageEntries, err = s.TogglClient.GetEntriesV2(toggl.DetailedReportParams{ |
||
140 | Page: currentPage, |
||
141 | Since: time.Now().AddDate(0, 0, -7), |
||
142 | Until: time.Now().AddDate(0, 0, 1), |
||
143 | }) |
||
144 | if err != nil { |
||
145 | s.Logger.Printf("[WARN] failed to load entries") |
||
146 | break |
||
147 | } |
||
148 | if len(pageEntries) == 0 { |
||
149 | break |
||
150 | } |
||
151 | |||
152 | entries = append(entries, pageEntries...) |
||
153 | } |
||
154 | } |
||
155 | |||
156 | entries = s.TogglClient.SumEntriesGroup(s.TogglClient.GroupEntriesByTask(entries)) |
||
157 | |||
158 | //render.Status(r, status) |
||
159 | render.JSON(w, r, entries) |
||
160 | } |
||
161 | |||
162 | // GET /v1/config |
||
163 | func (s Server) getConfigCtrl(w http.ResponseWriter, r *http.Request) { |
||
164 | render.JSON(w, r, config.GetConfig()) |
||
165 | } |
||
166 | |||
167 | // POST /v1/config |
||
168 | func (s Server) updateConfigCtrl(w http.ResponseWriter, r *http.Request) { |
||
169 | // answer to OPTIONS request for content-type |
||
170 | if r.Method == "OPTIONS" { |
||
171 | if r.Header.Get("Access-Control-Request-Method") == "content-type" { |
||
172 | w.Header().Set("Content-Type", "application/json") |
||
173 | } |
||
174 | return |
||
175 | } |
||
176 | |||
177 | newConfig := config.GetConfig() |
||
178 | decoder := json.NewDecoder(r.Body) |
||
179 | err := decoder.Decode(&newConfig) |
||
180 | if err != nil { |
||
181 | s.Logger.Printf("[ERROR] Cannot decode %v", err.Error()) |
||
182 | } |
||
183 | |||
184 | errors, _ := newConfig.Validate() |
||
185 | //if len(errors) == 0 { |
||
186 | newConfig.SaveConfig() |
||
187 | //} |
||
188 | render.JSON(w, r, errors) |
||
189 | } |
||
190 | |||
191 | // POST /v1/config/reload |
||
192 | func (s *Server) reloadConfigCtrl(w http.ResponseWriter, r *http.Request) { |
||
193 | newConfig := config.GetConfig() |
||
194 | s.Config = &newConfig |
||
195 | s.TogglClient.Config = &newConfig |
||
196 | s.TogglClient.ReloadConfig() |
||
197 | } |
||
198 | |||
199 | type ValidatorStatus struct { |
||
200 | Ok bool `json:"ok"` |
||
201 | Errors []string `json:"errors"` |
||
202 | Data interface{} `json:"data"` |
||
203 | } |
||
204 | |||
205 | // GET /api/v1/validate/config |
||
206 | func (s Server) validateConfig(w http.ResponseWriter, r *http.Request) { |
||
207 | v := client.ConfigValidator{s.Config} |
||
208 | render.JSON(w, r, client.StatusFromCheck(v.Check())) |
||
209 | } |
||
210 | |||
211 | // GET /api/v1/validate/planfix/user |
||
212 | func (s Server) validatePlanfixUser(w http.ResponseWriter, r *http.Request) { |
||
213 | v := client.PlanfixUserValidator{s.TogglClient} |
||
214 | render.JSON(w, r, client.StatusFromCheck(v.Check())) |
||
215 | } |
||
216 | |||
217 | // GET /api/v1/validate/planfix/analitic |
||
218 | func (s Server) validatePlanfixAnalitic(w http.ResponseWriter, r *http.Request) { |
||
219 | v := client.PlanfixAnaliticValidator{s.TogglClient} |
||
220 | render.JSON(w, r, client.StatusFromCheck(v.Check())) |
||
221 | } |
||
222 | |||
223 | // GET /api/v1/validate/toggl/user |
||
224 | func (s Server) validateTogglUser(w http.ResponseWriter, r *http.Request) { |
||
225 | v := client.TogglUserValidator{s.TogglClient} |
||
226 | render.JSON(w, r, client.StatusFromCheck(v.Check())) |
||
227 | } |
||
228 | |||
229 | // GET /api/v1/validate/toggl/workspace |
||
230 | func (s Server) validateTogglWorkspace(w http.ResponseWriter, r *http.Request) { |
||
231 | v := client.TogglWorkspaceValidator{s.TogglClient} |
||
232 | render.JSON(w, r, client.StatusFromCheck(v.Check())) |
||
233 | } |
||
234 | |||
235 | // GET /api/v1/planfix/user |
||
236 | func (s Server) getPlanfixUser(w http.ResponseWriter, r *http.Request) { |
||
237 | v := client.PlanfixUserValidator{s.TogglClient} |
||
238 | errors, ok, data := v.Check() |
||
239 | render.JSON(w, r, ValidatorStatus{ok, errors, data}) |
||
240 | } |
||
241 | |||
242 | // GET /api/v1/planfix/analitics |
||
243 | func (s Server) getPlanfixAnalitics(w http.ResponseWriter, r *http.Request) { |
||
244 | var analiticList planfix.XMLResponseAnaliticGetList |
||
245 | analiticList, err := s.TogglClient.PlanfixAPI.AnaliticGetList(0) |
||
246 | if err != nil { |
||
247 | render.Status(r, 400) |
||
248 | render.PlainText(w, r, err.Error()) |
||
249 | } |
||
250 | |||
251 | render.JSON(w, r, analiticList.Analitics) |
||
252 | } |
||
253 | |||
254 | // GET /api/v1/toggl/user |
||
255 | func (s Server) getTogglUser(w http.ResponseWriter, r *http.Request) { |
||
256 | var user toggl.Account |
||
257 | var errors []string |
||
258 | user, err := s.TogglClient.Session.GetAccount() |
||
259 | if err != nil { |
||
260 | msg := "Не удалось получить Toggl UserID, проверьте TogglAPIToken, %s" |
||
261 | errors = append(errors, fmt.Sprintf(msg, err.Error())) |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
262 | } |
||
263 | |||
264 | render.JSON(w, r, ValidatorStatus{err == nil, errors, user.Data}) |
||
265 | } |
||
266 | |||
267 | // GET /api/v1/toggl/workspaces |
||
268 | func (s Server) getTogglWorkspaces(w http.ResponseWriter, r *http.Request) { |
||
269 | var workspaces []toggl.Workspace |
||
270 | var errors []string |
||
271 | workspaces, err := s.TogglClient.Session.GetWorkspaces() |
||
272 | if err != nil { |
||
273 | msg := "Не удалось получить Toggl workspaces, проверьте TogglAPIToken, %s" |
||
274 | errors = append(errors, fmt.Sprintf(msg, err.Error())) |
||
0 ignored issues
–
show
|
|||
275 | } |
||
276 | |||
277 | render.JSON(w, r, ValidatorStatus{err == nil, errors, workspaces}) |
||
278 | } |
||
279 | |||
280 | // GET /toggl/entries/current |
||
281 | func (s Server) getTogglCurrentCtrl(w http.ResponseWriter, r *http.Request) { |
||
282 | entry, _ := s.TogglClient.GetCurrentEntry() |
||
283 | render.JSON(w, r, entry) |
||
284 | } |
||
285 | |||
286 | // GET /toggl/entries/planfix/{taskID} |
||
287 | func (s Server) getPlanfixTaskCtrl(w http.ResponseWriter, r *http.Request) { |
||
288 | taskID := chi.URLParam(r, "taskID") |
||
289 | entries, _ := s.TogglClient.GetEntriesByTag(taskID) |
||
290 | render.JSON(w, r, entries) |
||
291 | } |
||
292 | |||
293 | // GET /toggl/entries/planfix/{taskID}/last |
||
294 | func (s Server) getPlanfixTaskLastCtrl(w http.ResponseWriter, r *http.Request) { |
||
295 | taskID := chi.URLParam(r, "taskID") |
||
296 | entries, _ := s.TogglClient.GetEntriesByTag(taskID) |
||
297 | if len(entries) > 0 { |
||
298 | render.JSON(w, r, entries[0]) |
||
299 | } else { |
||
300 | render.JSON(w, r, entries) |
||
301 | } |
||
302 | } |
||
303 | |||
304 | // serves static files from ./docroot |
||
305 | func (s Server) fileServer(r chi.Router, path string, root http.FileSystem) { |
||
306 | //s.Logger.Printf("[INFO] run file server for %s", root) |
||
307 | fs := http.StripPrefix(path, http.FileServer(root)) |
||
308 | if path != "/" && path[len(path)-1] != '/' { |
||
309 | r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) |
||
310 | path += "/" |
||
311 | } |
||
312 | path += "*" |
||
313 | |||
314 | r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||
315 | // don't show dirs, just serve files |
||
316 | if strings.HasSuffix(r.URL.Path, "/") && len(r.URL.Path) > 1 && r.URL.Path != "/show/" { |
||
317 | http.NotFound(w, r) |
||
318 | return |
||
319 | } |
||
320 | fs.ServeHTTP(w, r) |
||
321 | })) |
||
322 | } |
||
323 |