feat: implement authentication flow with OAuth login page

This commit is contained in:
garionion (aider) 2025-02-26 00:11:06 +01:00
parent 10b2056954
commit 77538a7dfd
5 changed files with 146 additions and 10 deletions

32
main.go
View file

@ -89,7 +89,7 @@ func main() {
AuthURL: cfg.OAuth.AuthURL, AuthURL: cfg.OAuth.AuthURL,
TokenURL: cfg.OAuth.TokenURL, TokenURL: cfg.OAuth.TokenURL,
}, },
RedirectURL: cfg.OAuth.AuthURL, RedirectURL: fmt.Sprintf("http://localhost:%d/oauth/callback", cfg.Server.Port),
Scopes: []string{"profile", "email"}, Scopes: []string{"profile", "email"},
} }
@ -127,9 +127,7 @@ func main() {
e := echo.New() e := echo.New()
e.Use(middleware.Logger()) e.Use(middleware.Logger())
e.Use(middleware.Recover()) e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Serve static files
e.GET("/*", echo.WrapHandler(http.FileServer(http.FS(staticFiles))))
// OAuth2 routes // OAuth2 routes
e.GET("/oauth/login", handleOAuthLogin) 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 // Start server
addr := fmt.Sprintf(":%d", cfg.Server.Port) addr := fmt.Sprintf(":%d", cfg.Server.Port)
logger.Info("Starting server", zap.Int("port", 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)) logger.Info("OAuth token exchanged successfully", zap.String("access_token", token.AccessToken))
return c.JSON(http.StatusOK, map[string]interface{}{
"access_token": token.AccessToken, // Return HTML that stores the token and redirects to the main app
"expires_in": token.Expiry, html := fmt.Sprintf(`
}) <!DOCTYPE html>
<html>
<head>
<title>Authentication Successful</title>
<script>
localStorage.setItem('auth_token', '%s');
window.location.href = '/';
</script>
</head>
<body>
<p>Authentication successful. Redirecting...</p>
</body>
</html>
`, token.AccessToken)
return c.HTML(http.StatusOK, html)
} }
// Middleware to enforce OAuth authentication // Middleware to enforce OAuth authentication

View file

@ -1,9 +1,21 @@
<template> <template>
<v-app> <v-app>
<router-view /> <v-main>
<router-view />
</v-main>
</v-app> </v-app>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// import { useAuthStore } from '@/stores/auth';
import { onMounted } from 'vue';
const authStore = useAuthStore();
onMounted(async () => {
// If user has a token, fetch user info
if (authStore.isAuthenticated) {
await authStore.fetchUserInfo();
}
});
</script> </script>

39
web/src/pages/auth.vue Normal file
View file

@ -0,0 +1,39 @@
<template>
<v-container class="fill-height">
<v-responsive class="align-center fill-height mx-auto" max-width="500">
<v-card class="pa-6" elevation="8">
<div class="text-center mb-6">
<v-img
class="mx-auto mb-6"
height="100"
src="@/assets/logo.png"
width="100"
/>
<h1 class="text-h4 font-weight-bold mb-2">Welcome</h1>
<p class="text-body-1">Please sign in to continue</p>
</div>
<v-divider class="mb-6"></v-divider>
<v-btn
block
color="primary"
size="large"
@click="login"
prepend-icon="mdi-login"
>
Sign in with OAuth
</v-btn>
</v-card>
</v-responsive>
</v-container>
</template>
<script lang="ts" setup>
const router = useRouter();
function login() {
// Redirect to the OAuth login endpoint
window.location.href = '/oauth/login';
}
</script>

View file

@ -14,6 +14,22 @@ const router = createRouter({
routes: setupLayouts(routes), 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 // Workaround for https://github.com/vitejs/vite/issues/11804
router.onError((err, to) => { router.onError((err, to) => {
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) { if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {

53
web/src/stores/auth.ts Normal file
View file

@ -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();
}
}
}
})