From 77538a7dfd1f4d95f40c0b056f03b086c9088128 Mon Sep 17 00:00:00 2001 From: "garionion (aider)" Date: Wed, 26 Feb 2025 00:11:06 +0100 Subject: [PATCH] feat: implement authentication flow with OAuth login page --- main.go | 32 ++++++++++++++++++------- web/src/App.vue | 16 +++++++++++-- web/src/pages/auth.vue | 39 ++++++++++++++++++++++++++++++ web/src/router/index.ts | 16 +++++++++++++ web/src/stores/auth.ts | 53 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 web/src/pages/auth.vue create mode 100644 web/src/stores/auth.ts diff --git a/main.go b/main.go index fe42959..4c33ad4 100644 --- a/main.go +++ b/main.go @@ -89,7 +89,7 @@ func main() { AuthURL: cfg.OAuth.AuthURL, TokenURL: cfg.OAuth.TokenURL, }, - RedirectURL: cfg.OAuth.AuthURL, + RedirectURL: fmt.Sprintf("http://localhost:%d/oauth/callback", cfg.Server.Port), Scopes: []string{"profile", "email"}, } @@ -127,9 +127,7 @@ func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) - - // Serve static files - e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles)))) + e.Use(middleware.CORS()) // OAuth2 routes e.GET("/oauth/login", handleOAuthLogin) @@ -146,6 +144,9 @@ func main() { }) }) + // Serve static files - must be after API routes + e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles)))) + // Start server addr := fmt.Sprintf(":%d", cfg.Server.Port) logger.Info("Starting server", zap.Int("port", cfg.Server.Port)) @@ -190,10 +191,25 @@ func handleOAuthCallback(c echo.Context) error { } logger.Info("OAuth token exchanged successfully", zap.String("access_token", token.AccessToken)) - return c.JSON(http.StatusOK, map[string]interface{}{ - "access_token": token.AccessToken, - "expires_in": token.Expiry, - }) + + // Return HTML that stores the token and redirects to the main app + html := fmt.Sprintf(` + + + + Authentication Successful + + + +

Authentication successful. Redirecting...

+ + + `, token.AccessToken) + + return c.HTML(http.StatusOK, html) } // Middleware to enforce OAuth authentication diff --git a/web/src/App.vue b/web/src/App.vue index 9497da7..248a2cc 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,9 +1,21 @@ diff --git a/web/src/pages/auth.vue b/web/src/pages/auth.vue new file mode 100644 index 0000000..e671e50 --- /dev/null +++ b/web/src/pages/auth.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 5d4b8bc..6bdab2f 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -14,6 +14,22 @@ const router = createRouter({ routes: setupLayouts(routes), }) +// Add authentication guard +router.beforeEach((to, from, next) => { + // Check if the user is authenticated + const isAuthenticated = !!localStorage.getItem('auth_token'); + + // If route requires auth and user is not authenticated, redirect to auth page + if (to.path !== '/auth' && !isAuthenticated) { + next('/auth'); + } else if (to.path === '/auth' && isAuthenticated) { + // If user is already authenticated and tries to access auth page, redirect to home + next('/'); + } else { + next(); + } +}); + // Workaround for https://github.com/vitejs/vite/issues/11804 router.onError((err, to) => { if (err?.message?.includes?.('Failed to fetch dynamically imported module')) { diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts new file mode 100644 index 0000000..f72ef7d --- /dev/null +++ b/web/src/stores/auth.ts @@ -0,0 +1,53 @@ +// Auth store to manage authentication state +import { defineStore } from 'pinia' + +export const useAuthStore = defineStore('auth', { + state: () => ({ + token: localStorage.getItem('auth_token') || null, + user: null as any | null, + }), + + getters: { + isAuthenticated: (state) => !!state.token, + }, + + actions: { + setToken(token: string) { + this.token = token; + localStorage.setItem('auth_token', token); + }, + + setUser(user: any) { + this.user = user; + }, + + logout() { + this.token = null; + this.user = null; + localStorage.removeItem('auth_token'); + }, + + async fetchUserInfo() { + if (!this.token) return; + + try { + const response = await fetch('/api/v1/health', { + headers: { + 'Authorization': `Bearer ${this.token}` + } + }); + + if (response.ok) { + const data = await response.json(); + this.setUser(data.user); + } else { + // If token is invalid, logout + this.logout(); + } + } catch (error) { + console.error('Failed to fetch user info:', error); + this.logout(); + } + } + } +})