From 22c69fddef1faa634ef4219f3bc61839a4a8b0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bahattin=20=C3=87ini=C3=A7?= Date: Thu, 23 May 2024 16:34:07 +0300 Subject: [PATCH] feat(ui): Login and sync flow have been implemented --- api/api.go | 5 ++ api/docs/docs.go | 4 +- api/docs/swagger.json | 4 +- api/docs/swagger.yaml | 4 +- queue/queue.go | 10 +-- strava/user.go | 4 +- ui/package-lock.json | 9 +++ ui/package.json | 1 + ui/src/App.vue | 2 +- ui/src/pages/LoginPage.vue | 81 ++++++++++++++++++++ ui/src/pages/SettingsPage.vue | 137 ++++++++++++++++++++++++++++------ ui/src/router/index.js | 22 ++++++ ui/src/services/api.js | 3 +- ui/src/services/auth.js | 34 +++++++++ ui/src/services/user.js | 47 ++++++++++++ ui/src/store/user.js | 16 ++++ ui/store/index.js | 12 --- 17 files changed, 344 insertions(+), 51 deletions(-) create mode 100644 ui/src/pages/LoginPage.vue create mode 100644 ui/src/services/auth.js create mode 100644 ui/src/store/user.js delete mode 100644 ui/store/index.js diff --git a/api/api.go b/api/api.go index 2ed553b..0d292c7 100644 --- a/api/api.go +++ b/api/api.go @@ -15,6 +15,7 @@ import ( "github.com/bahattincinic/fitwave/queue" "github.com/bahattincinic/fitwave/strava" "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" "go.uber.org/zap" ) @@ -44,6 +45,10 @@ func RunAPI(ctx context.Context, wg *sync.WaitGroup, log *zap.Logger, db *databa } srv.ec.Server.IdleTimeout = 120 * time.Second + srv.ec.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + Format: "method=${method}, uri=${uri}, status=${status}, error=${error}\n", + })) + srv.setupHandlers() srv.setupSwagger() srv.setupCors() diff --git a/api/docs/docs.go b/api/docs/docs.go index 799f4e9..a4fbe89 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -1359,7 +1359,7 @@ const docTemplate = `{ "queue.TaskResult": { "type": "object", "properties": { - "completionTime": { + "completion_time": { "type": "string" }, "error": {}, @@ -1613,7 +1613,7 @@ const docTemplate = `{ "strava.User": { "type": "object", "properties": { - "accessToken": { + "access_token": { "type": "string" }, "athlete": { diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 6f3ed37..7df6408 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -1348,7 +1348,7 @@ "queue.TaskResult": { "type": "object", "properties": { - "completionTime": { + "completion_time": { "type": "string" }, "error": {}, @@ -1602,7 +1602,7 @@ "strava.User": { "type": "object", "properties": { - "accessToken": { + "access_token": { "type": "string" }, "athlete": { diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index cbe7451..fae649a 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -265,7 +265,7 @@ definitions: type: object queue.TaskResult: properties: - completionTime: + completion_time: type: string error: {} id: @@ -438,7 +438,7 @@ definitions: type: object strava.User: properties: - accessToken: + access_token: type: string athlete: $ref: '#/definitions/strava.AthleteDetailed' diff --git a/queue/queue.go b/queue/queue.go index 2a4e214..373fec3 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -30,11 +30,11 @@ type Task struct { } type TaskResult struct { - ID string - Status TaskStatus - Error error - CompletionTime time.Time - Result interface{} + ID string `json:"id"` + Status TaskStatus `json:"status"` + Error error `json:"error"` + CompletionTime time.Time `json:"completion_time"` + Result interface{} `json:"result"` } // Queue represents a task queue. diff --git a/strava/user.go b/strava/user.go index 28c8c74..d9b4849 100644 --- a/strava/user.go +++ b/strava/user.go @@ -8,9 +8,9 @@ import ( type User struct { st *client.Client - AccessToken string + AccessToken string `json:"access_token"` cfg *models.Config - Athlete *client.AthleteDetailed + Athlete *client.AthleteDetailed `json:"athlete"` } func (s *Strava) NewUser(cfg *models.Config, accessToken string) (*User, error) { diff --git a/ui/package-lock.json b/ui/package-lock.json index db22704..f22718f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@vueuse/head": "^2.0.0", "core-js": "^3.8.3", + "js-cookie": "^3.0.5", "pinia": "^2.1.7", "primeicons": "^7.0.0", "primevue": "^3.52.0", @@ -6933,6 +6934,14 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-message": { "version": "1.0.7", "resolved": "https://registry.npmmirror.com/js-message/-/js-message-1.0.7.tgz", diff --git a/ui/package.json b/ui/package.json index 212b018..8ea5b39 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,6 +10,7 @@ "dependencies": { "@vueuse/head": "^2.0.0", "core-js": "^3.8.3", + "js-cookie": "^3.0.5", "pinia": "^2.1.7", "primeicons": "^7.0.0", "primevue": "^3.52.0", diff --git a/ui/src/App.vue b/ui/src/App.vue index 787b806..d92d7f5 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -1,5 +1,5 @@ diff --git a/ui/src/router/index.js b/ui/src/router/index.js index fdd00fa..41c2d19 100644 --- a/ui/src/router/index.js +++ b/ui/src/router/index.js @@ -1,12 +1,17 @@ import { createRouter, createWebHistory } from 'vue-router'; +import Cookies from 'js-cookie'; +import LoginPage from '@/pages/LoginPage'; import HomePage from '@/pages/HomePage'; import SettingsPage from '@/pages/SettingsPage'; import AthletesPage from '@/pages/AthletesPage'; import ActivitiesPage from '@/pages/ActivitiesPage'; import GearsPage from '@/pages/GearsPage'; +import { useUserStore } from '@/store/user'; +import { getUserMe } from "@/services/user"; const routes = [ { path: '/', name: 'Home', component: HomePage }, + { path: '/login', name: 'Login', component: LoginPage }, { path: '/settings', name: 'Settings', component: SettingsPage }, { path: '/activities', name: 'Activities', component: ActivitiesPage }, { path: '/athletes', name: 'Athletes', component: AthletesPage }, @@ -18,4 +23,21 @@ const router = createRouter({ routes }); +router.beforeEach(async (to, from, next) => { + const userStore = useUserStore(); + const accessToken = Cookies.get('accessToken'); + + if (userStore.accessToken === '' && accessToken) { + try { + const resp = await getUserMe(accessToken); + userStore.setAccessToken(accessToken); + userStore.setUser(resp.athlete); + } catch { + userStore.setAccessToken(''); + Cookies.remove('accessToken'); + } + } + next(); +}); + export default router; diff --git a/ui/src/services/api.js b/ui/src/services/api.js index c45b246..3890609 100644 --- a/ui/src/services/api.js +++ b/ui/src/services/api.js @@ -1,3 +1,4 @@ const API_BASE_URL = process.env.API_URL || 'http://localhost:9000/api'; +const CALLBACK_URL = process.env.CALLBACK_URL || 'http://localhost:8080/login'; -export { API_BASE_URL }; +export { API_BASE_URL, CALLBACK_URL }; diff --git a/ui/src/services/auth.js b/ui/src/services/auth.js new file mode 100644 index 0000000..a21d8cf --- /dev/null +++ b/ui/src/services/auth.js @@ -0,0 +1,34 @@ +import { API_BASE_URL, CALLBACK_URL } from './api'; + +export async function getAuthorizationURL() { + const endpoint = `${API_BASE_URL}/auth/authorization-url?callback_url=${CALLBACK_URL}`; + const response = await fetch(endpoint, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Could not fetch user config'); + } + + return await response.json(); +} + +export async function getAccessToken(code) { + const endpoint = `${API_BASE_URL}/auth/token`; + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ code }) + }); + + if (!response.ok) { + throw new Error('Could not fetch access token'); + } + + return await response.json(); +} diff --git a/ui/src/services/user.js b/ui/src/services/user.js index d606024..4021b01 100644 --- a/ui/src/services/user.js +++ b/ui/src/services/user.js @@ -15,6 +15,53 @@ export async function getUserConfig() { return await response.json(); } +export async function getUserMe(accessToken) { + const response = await fetch(`${API_BASE_URL}/user/me`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}` + } + }); + + if (!response.ok) { + throw new Error('Could not fetch user'); + } + + return await response.json(); +} + +export async function getTaskDetail(id) { + const response = await fetch(`${API_BASE_URL}/user/task/${id}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (!response.ok) { + throw new Error('Could not fetch task detail'); + } + + return await response.json(); +} + +export async function triggerSync(accessToken) { + const response = await fetch(`${API_BASE_URL}/user/sync`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}` + } + }); + + if (!response.ok) { + throw new Error('Could not fetch task detail'); + } + + return await response.json(); +} + export async function saveUserConfig(config) { const response = await fetch(`${API_BASE_URL}/user/config`, { method: 'PUT', diff --git a/ui/src/store/user.js b/ui/src/store/user.js new file mode 100644 index 0000000..b007990 --- /dev/null +++ b/ui/src/store/user.js @@ -0,0 +1,16 @@ +import { defineStore } from 'pinia'; + +export const useUserStore = defineStore('user', { + state: () => ({ + accessToken: '', + user: {} + }), + actions: { + setUser(user) { + this.user = user; + }, + setAccessToken(accessToken) { + this.accessToken = accessToken; + } + } +}); diff --git a/ui/store/index.js b/ui/store/index.js deleted file mode 100644 index b22e061..0000000 --- a/ui/store/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import { defineStore } from 'pinia'; - -export const useMainStore = defineStore('main', { - state: () => ({ - message: 'Hello from Pinia!' - }), - actions: { - updateMessage(newMessage) { - this.message = newMessage; - } - } -});